From 9b71394cf9d7971e8594b8dcbe5b21a317c48ba4 Mon Sep 17 00:00:00 2001 From: harkamal Date: Wed, 24 Jan 2024 17:38:11 +0530 Subject: [PATCH 01/51] feat: placeholder PR for electra add types stub and epoch config fix types --- .../test/spec/presets/fork.test.ts | 2 + .../test/spec/presets/transition.test.ts | 8 + .../upgradeLightClientHeader.test.ts | 33 +++- .../test/unit/network/fork.test.ts | 13 +- packages/beacon-node/test/utils/config.ts | 8 + .../config/src/chainConfig/configs/mainnet.ts | 4 + .../config/src/chainConfig/configs/minimal.ts | 4 + packages/config/src/chainConfig/types.ts | 6 + packages/config/src/forkConfig/index.ts | 10 +- packages/light-client/src/spec/utils.ts | 4 + packages/params/src/forkName.ts | 2 + .../test/unit/upgradeState.test.ts | 8 + packages/types/src/allForks/sszTypes.ts | 38 +++++ packages/types/src/electra/index.ts | 3 + packages/types/src/electra/sszTypes.ts | 148 ++++++++++++++++++ packages/types/src/electra/types.ts | 29 ++++ packages/types/src/sszTypes.ts | 1 + packages/types/src/types.ts | 1 + packages/validator/src/util/params.ts | 5 + 19 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 packages/types/src/electra/index.ts create mode 100644 packages/types/src/electra/sszTypes.ts create mode 100644 packages/types/src/electra/types.ts diff --git a/packages/beacon-node/test/spec/presets/fork.test.ts b/packages/beacon-node/test/spec/presets/fork.test.ts index 228ab6a38935..c880d24bbbe3 100644 --- a/packages/beacon-node/test/spec/presets/fork.test.ts +++ b/packages/beacon-node/test/spec/presets/fork.test.ts @@ -35,6 +35,8 @@ const fork: TestRunnerFn = (forkNext) => { return slotFns.upgradeStateToCapella(preState as CachedBeaconStateBellatrix); case ForkName.deneb: return slotFns.upgradeStateToDeneb(preState as CachedBeaconStateCapella); + case ForkName.electra: + throw Error("not Implemented"); } }, options: { diff --git a/packages/beacon-node/test/spec/presets/transition.test.ts b/packages/beacon-node/test/spec/presets/transition.test.ts index 77919d76c3b1..1f98dbb41f58 100644 --- a/packages/beacon-node/test/spec/presets/transition.test.ts +++ b/packages/beacon-node/test/spec/presets/transition.test.ts @@ -102,6 +102,14 @@ function getTransitionConfig(fork: ForkName, forkEpoch: number): Partial${toFork}`, function () { + lcHeaderByFork[fromFork].beacon.slot = testSlots[fromFork]; + lcHeaderByFork[toFork].beacon.slot = testSlots[fromFork]; + + expect(() => { + upgradeLightClientHeader(config, toFork, lcHeaderByFork[fromFork]); + }).toThrow("Not Implemented"); + }); + } + + // Since electra is not implemented for loop is till deneb (Object.values(ForkName).length-1) + // Once electra is implemnted run for loop till Object.values(ForkName).length + + // for (let i = ForkSeq.altair; i < Object.values(ForkName).length; i++) { + + for (let i = ForkSeq.altair; i < Object.values(ForkName).length - 1; i++) { for (let j = i; j > 0; j--) { const fromFork = ForkName[ForkSeq[i] as ForkName]; const toFork = ForkName[ForkSeq[j] as ForkName]; diff --git a/packages/beacon-node/test/unit/network/fork.test.ts b/packages/beacon-node/test/unit/network/fork.test.ts index be748d2e8185..bbe1c0870d30 100644 --- a/packages/beacon-node/test/unit/network/fork.test.ts +++ b/packages/beacon-node/test/unit/network/fork.test.ts @@ -9,12 +9,14 @@ function getForkConfig({ bellatrix, capella, deneb, + electra, }: { phase0: number; altair: number; bellatrix: number; capella: number; deneb: number; + electra: number; }): BeaconConfig { const forks: Record = { phase0: { @@ -57,6 +59,14 @@ function getForkConfig({ prevVersion: Buffer.from([0, 0, 0, 3]), prevForkName: ForkName.capella, }, + electra: { + name: ForkName.electra, + seq: ForkSeq.electra, + epoch: electra, + version: Buffer.from([0, 0, 0, 5]), + prevVersion: Buffer.from([0, 0, 0, 4]), + prevForkName: ForkName.deneb, + }, }; const forksAscendingEpochOrder = Object.values(forks); const forksDescendingEpochOrder = Object.values(forks).reverse(); @@ -133,9 +143,10 @@ const testScenarios = [ for (const testScenario of testScenarios) { const {phase0, altair, bellatrix, capella, testCases} = testScenario; const deneb = Infinity; + const electra = Infinity; describe(`network / fork: phase0: ${phase0}, altair: ${altair}, bellatrix: ${bellatrix} capella: ${capella}`, () => { - const forkConfig = getForkConfig({phase0, altair, bellatrix, capella, deneb}); + const forkConfig = getForkConfig({phase0, altair, bellatrix, capella, deneb, electra}); const forks = forkConfig.forks; for (const testCase of testCases) { const {epoch, currentFork, nextFork, activeForks} = testCase; diff --git a/packages/beacon-node/test/utils/config.ts b/packages/beacon-node/test/utils/config.ts index 54c058d30722..2aad1c14c03e 100644 --- a/packages/beacon-node/test/utils/config.ts +++ b/packages/beacon-node/test/utils/config.ts @@ -31,5 +31,13 @@ export function getConfig(fork: ForkName, forkEpoch = 0): ChainForkConfig { CAPELLA_FORK_EPOCH: 0, DENEB_FORK_EPOCH: forkEpoch, }); + case ForkName.electra: + return createChainForkConfig({ + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: forkEpoch, + }); } } diff --git a/packages/config/src/chainConfig/configs/mainnet.ts b/packages/config/src/chainConfig/configs/mainnet.ts index 883688ca821b..0de1bee666ec 100644 --- a/packages/config/src/chainConfig/configs/mainnet.ts +++ b/packages/config/src/chainConfig/configs/mainnet.ts @@ -49,6 +49,10 @@ export const chainConfig: ChainConfig = { DENEB_FORK_VERSION: b("0x04000000"), DENEB_FORK_EPOCH: 269568, // March 13, 2024, 01:55:35pm UTC + // Electra + ELECTRA_FORK_VERSION: b("0x05000000"), + ELECTRA_FORK_EPOCH: Infinity, + // Time parameters // --------------------------------------------------------------- // 12 seconds diff --git a/packages/config/src/chainConfig/configs/minimal.ts b/packages/config/src/chainConfig/configs/minimal.ts index 23cd14e763ec..c99a76d1ee40 100644 --- a/packages/config/src/chainConfig/configs/minimal.ts +++ b/packages/config/src/chainConfig/configs/minimal.ts @@ -46,6 +46,10 @@ export const chainConfig: ChainConfig = { DENEB_FORK_VERSION: b("0x04000001"), DENEB_FORK_EPOCH: Infinity, + // Electra + ELECTRA_FORK_VERSION: b("0x05000001"), + ELECTRA_FORK_EPOCH: Infinity, + // Time parameters // --------------------------------------------------------------- // [customized] Faster for testing purposes diff --git a/packages/config/src/chainConfig/types.ts b/packages/config/src/chainConfig/types.ts index 45f05bfaa724..234a08558be5 100644 --- a/packages/config/src/chainConfig/types.ts +++ b/packages/config/src/chainConfig/types.ts @@ -40,6 +40,9 @@ export type ChainConfig = { // DENEB DENEB_FORK_VERSION: Uint8Array; DENEB_FORK_EPOCH: number; + // ELECTRA + ELECTRA_FORK_VERSION: Uint8Array; + ELECTRA_FORK_EPOCH: number; // Time parameters SECONDS_PER_SLOT: number; @@ -99,6 +102,9 @@ export const chainConfigTypes: SpecTypes = { // DENEB DENEB_FORK_VERSION: "bytes", DENEB_FORK_EPOCH: "number", + // ELECTRA + ELECTRA_FORK_VERSION: "bytes", + ELECTRA_FORK_EPOCH: "number", // Time parameters SECONDS_PER_SLOT: "number", diff --git a/packages/config/src/forkConfig/index.ts b/packages/config/src/forkConfig/index.ts index d630f1ddfc88..efb27ca1505f 100644 --- a/packages/config/src/forkConfig/index.ts +++ b/packages/config/src/forkConfig/index.ts @@ -55,10 +55,18 @@ export function createForkConfig(config: ChainConfig): ForkConfig { prevVersion: config.CAPELLA_FORK_VERSION, prevForkName: ForkName.capella, }; + const electra: ForkInfo = { + name: ForkName.electra, + seq: ForkSeq.electra, + epoch: config.ELECTRA_FORK_EPOCH, + version: config.ELECTRA_FORK_VERSION, + prevVersion: config.DENEB_FORK_VERSION, + prevForkName: ForkName.deneb, + }; /** Forks in order order of occurence, `phase0` first */ // Note: Downstream code relies on proper ordering. - const forks = {phase0, altair, bellatrix, capella, deneb}; + const forks = {phase0, altair, bellatrix, capella, deneb, electra}; // Prevents allocating an array on every getForkInfo() call const forksAscendingEpochOrder = Object.values(forks); diff --git a/packages/light-client/src/spec/utils.ts b/packages/light-client/src/spec/utils.ts index 2a5720a1f637..ed4b9e6e961d 100644 --- a/packages/light-client/src/spec/utils.ts +++ b/packages/light-client/src/spec/utils.ts @@ -103,6 +103,10 @@ export function upgradeLightClientHeader( // Break if no further upgradation is required else fall through if (ForkSeq[targetFork] <= ForkSeq.deneb) break; + + // eslint-disable-next-line no-fallthrough + case ForkName.electra: + throw Error("Not Implemented"); } return upgradedHeader; } diff --git a/packages/params/src/forkName.ts b/packages/params/src/forkName.ts index 142684c313f4..bbb72a7972fa 100644 --- a/packages/params/src/forkName.ts +++ b/packages/params/src/forkName.ts @@ -7,6 +7,7 @@ export enum ForkName { bellatrix = "bellatrix", capella = "capella", deneb = "deneb", + electra = "electra", } /** @@ -18,6 +19,7 @@ export enum ForkSeq { bellatrix = 2, capella = 3, deneb = 4, + electra = 5, } export type ForkPreLightClient = ForkName.phase0; diff --git a/packages/state-transition/test/unit/upgradeState.test.ts b/packages/state-transition/test/unit/upgradeState.test.ts index 2ea8eef182ac..75ba415c1bea 100644 --- a/packages/state-transition/test/unit/upgradeState.test.ts +++ b/packages/state-transition/test/unit/upgradeState.test.ts @@ -55,5 +55,13 @@ function getConfig(fork: ForkName, forkEpoch = 0): ChainForkConfig { CAPELLA_FORK_EPOCH: 0, DENEB_FORK_EPOCH: forkEpoch, }); + case ForkName.electra: + return createChainForkConfig({ + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: forkEpoch, + }); } } diff --git a/packages/types/src/allForks/sszTypes.ts b/packages/types/src/allForks/sszTypes.ts index 7174bc52e89c..6030215ac8ca 100644 --- a/packages/types/src/allForks/sszTypes.ts +++ b/packages/types/src/allForks/sszTypes.ts @@ -3,6 +3,7 @@ import {ssz as altair} from "../altair/index.js"; import {ssz as bellatrix} from "../bellatrix/index.js"; import {ssz as capella} from "../capella/index.js"; import {ssz as deneb} from "../deneb/index.js"; +import {ssz as electra} from "../electra/index.js"; /** * Index the ssz types that differ by fork @@ -44,6 +45,13 @@ export const allForks = { BeaconState: deneb.BeaconState, Metadata: altair.Metadata, }, + electra: { + BeaconBlockBody: electra.BeaconBlockBody, + BeaconBlock: electra.BeaconBlock, + SignedBeaconBlock: electra.SignedBeaconBlock, + BeaconState: electra.BeaconState, + Metadata: altair.Metadata, + }, }; /** @@ -85,6 +93,17 @@ export const allForksExecution = { SignedBuilderBid: deneb.SignedBuilderBid, SSEPayloadAttributes: deneb.SSEPayloadAttributes, }, + electra: { + BeaconBlockBody: electra.BeaconBlockBody, + BeaconBlock: electra.BeaconBlock, + SignedBeaconBlock: electra.SignedBeaconBlock, + BeaconState: electra.BeaconState, + ExecutionPayload: electra.ExecutionPayload, + ExecutionPayloadHeader: electra.ExecutionPayloadHeader, + BuilderBid: electra.BuilderBid, + SignedBuilderBid: electra.SignedBuilderBid, + SSEPayloadAttributes: electra.SSEPayloadAttributes, + }, }; /** @@ -107,6 +126,11 @@ export const allForksBlinded = { BeaconBlock: deneb.BlindedBeaconBlock, SignedBeaconBlock: deneb.SignedBlindedBeaconBlock, }, + electra: { + BeaconBlockBody: electra.BlindedBeaconBlockBody, + BeaconBlock: electra.BlindedBeaconBlock, + SignedBeaconBlock: electra.SignedBlindedBeaconBlock, + }, }; export const allForksLightClient = { @@ -150,6 +174,16 @@ export const allForksLightClient = { LightClientOptimisticUpdate: deneb.LightClientOptimisticUpdate, LightClientStore: deneb.LightClientStore, }, + electra: { + BeaconBlock: electra.BeaconBlock, + BeaconBlockBody: electra.BeaconBlockBody, + LightClientHeader: electra.LightClientHeader, + LightClientBootstrap: electra.LightClientBootstrap, + LightClientUpdate: electra.LightClientUpdate, + LightClientFinalityUpdate: electra.LightClientFinalityUpdate, + LightClientOptimisticUpdate: electra.LightClientOptimisticUpdate, + LightClientStore: electra.LightClientStore, + }, }; export const allForksBlobs = { @@ -157,4 +191,8 @@ export const allForksBlobs = { BlobSidecar: deneb.BlobSidecar, ExecutionPayloadAndBlobsBundle: deneb.ExecutionPayloadAndBlobsBundle, }, + electra: { + BlobSidecar: electra.BlobSidecar, + ExecutionPayloadAndBlobsBundle: electra.ExecutionPayloadAndBlobsBundle, + }, }; diff --git a/packages/types/src/electra/index.ts b/packages/types/src/electra/index.ts new file mode 100644 index 000000000000..7856cd729620 --- /dev/null +++ b/packages/types/src/electra/index.ts @@ -0,0 +1,3 @@ +export * from "./types.js"; +export * as ts from "./types.js"; +export * as ssz from "./sszTypes.js"; diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts new file mode 100644 index 000000000000..30690a499845 --- /dev/null +++ b/packages/types/src/electra/sszTypes.ts @@ -0,0 +1,148 @@ +import {ContainerType} from "@chainsafe/ssz"; +import {ssz as primitiveSsz} from "../primitive/index.js"; +import {ssz as denebSsz} from "../deneb/index.js"; + +const {BLSSignature} = primitiveSsz; + +export const ExecutionPayload = new ContainerType( + { + ...denebSsz.ExecutionPayload.fields, + }, + {typeName: "ExecutionPayload", jsonCase: "eth2"} +); + +export const ExecutionPayloadHeader = new ContainerType( + { + ...denebSsz.ExecutionPayloadHeader.fields, + }, + {typeName: "ExecutionPayloadHeader", jsonCase: "eth2"} +); + +export const BeaconBlockBody = new ContainerType( + { + ...denebSsz.BeaconBlockBody.fields, + }, + {typeName: "BeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} +); + +export const BeaconBlock = new ContainerType( + { + ...denebSsz.BeaconBlock.fields, + }, + {typeName: "BeaconBlock", jsonCase: "eth2", cachePermanentRootStruct: true} +); + +export const SignedBeaconBlock = new ContainerType( + { + message: BeaconBlock, + signature: BLSSignature, + }, + {typeName: "SignedBeaconBlock", jsonCase: "eth2"} +); + +export const BlobSidecar = new ContainerType( + { + ...denebSsz.BlobSidecar.fields, + }, + {typeName: "BlobSidecar", jsonCase: "eth2"} +); + +export const BlindedBeaconBlockBody = new ContainerType( + { + ...denebSsz.BlindedBeaconBlockBody.fields, + }, + {typeName: "BlindedBeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} +); + +export const BlindedBeaconBlock = new ContainerType( + { + ...denebSsz.BlindedBeaconBlock.fields, + }, + {typeName: "BlindedBeaconBlock", jsonCase: "eth2", cachePermanentRootStruct: true} +); + +export const SignedBlindedBeaconBlock = new ContainerType( + { + message: BlindedBeaconBlock, + signature: BLSSignature, + }, + {typeName: "SignedBlindedBeaconBlock", jsonCase: "eth2"} +); + +export const BuilderBid = new ContainerType( + { + ...denebSsz.BuilderBid.fields, + }, + {typeName: "BuilderBid", jsonCase: "eth2"} +); + +export const SignedBuilderBid = new ContainerType( + { + message: BuilderBid, + signature: BLSSignature, + }, + {typeName: "SignedBuilderBid", jsonCase: "eth2"} +); + +export const ExecutionPayloadAndBlobsBundle = new ContainerType( + { + ...denebSsz.ExecutionPayloadAndBlobsBundle.fields, + }, + {typeName: "ExecutionPayloadAndBlobsBundle", jsonCase: "eth2"} +); + +export const BeaconState = new ContainerType( + { + ...denebSsz.BeaconState.fields, + }, + {typeName: "BeaconState", jsonCase: "eth2"} +); + +export const LightClientHeader = new ContainerType( + { + ...denebSsz.LightClientHeader.fields, + }, + {typeName: "LightClientHeader", jsonCase: "eth2"} +); + +export const LightClientBootstrap = new ContainerType( + { + ...denebSsz.LightClientBootstrap.fields, + }, + {typeName: "LightClientBootstrap", jsonCase: "eth2"} +); + +export const LightClientUpdate = new ContainerType( + { + ...denebSsz.LightClientUpdate.fields, + }, + {typeName: "LightClientUpdate", jsonCase: "eth2"} +); + +export const LightClientFinalityUpdate = new ContainerType( + { + ...denebSsz.LightClientFinalityUpdate.fields, + }, + {typeName: "LightClientFinalityUpdate", jsonCase: "eth2"} +); + +export const LightClientOptimisticUpdate = new ContainerType( + { + ...denebSsz.LightClientOptimisticUpdate.fields, + }, + {typeName: "LightClientOptimisticUpdate", jsonCase: "eth2"} +); + +export const LightClientStore = new ContainerType( + { + ...denebSsz.LightClientStore.fields, + }, + {typeName: "LightClientStore", jsonCase: "eth2"} +); + +export const SSEPayloadAttributes = new ContainerType( + { + ...denebSsz.SSEPayloadAttributes.fields, + }, + {typeName: "SSEPayloadAttributes", jsonCase: "eth2"} +); diff --git a/packages/types/src/electra/types.ts b/packages/types/src/electra/types.ts new file mode 100644 index 000000000000..198259eed1dd --- /dev/null +++ b/packages/types/src/electra/types.ts @@ -0,0 +1,29 @@ +import {ValueOf} from "@chainsafe/ssz"; +import * as ssz from "./sszTypes.js"; + +export type BlobSidecar = ValueOf; +export type ExecutionPayloadAndBlobsBundle = ValueOf; + +export type ExecutionPayload = ValueOf; +export type ExecutionPayloadHeader = ValueOf; + +export type BeaconBlockBody = ValueOf; +export type BeaconBlock = ValueOf; +export type SignedBeaconBlock = ValueOf; + +export type BeaconState = ValueOf; + +export type BlindedBeaconBlockBody = ValueOf; +export type BlindedBeaconBlock = ValueOf; +export type SignedBlindedBeaconBlock = ValueOf; + +export type BuilderBid = ValueOf; +export type SignedBuilderBid = ValueOf; +export type SSEPayloadAttributes = ValueOf; + +export type LightClientHeader = ValueOf; +export type LightClientBootstrap = ValueOf; +export type LightClientUpdate = ValueOf; +export type LightClientFinalityUpdate = ValueOf; +export type LightClientOptimisticUpdate = ValueOf; +export type LightClientStore = ValueOf; diff --git a/packages/types/src/sszTypes.ts b/packages/types/src/sszTypes.ts index 2a7df948a447..ff3e77ab6947 100644 --- a/packages/types/src/sszTypes.ts +++ b/packages/types/src/sszTypes.ts @@ -4,6 +4,7 @@ export {ssz as altair} from "./altair/index.js"; export {ssz as bellatrix} from "./bellatrix/index.js"; export {ssz as capella} from "./capella/index.js"; export {ssz as deneb} from "./deneb/index.js"; +export {ssz as electra} from "./electra/index.js"; import {ssz as allForksSsz} from "./allForks/index.js"; export const allForks = allForksSsz.allForks; diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index e2e416fa3667..fbd7d5621da0 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -6,6 +6,7 @@ export {ts as altair} from "./altair/index.js"; export {ts as bellatrix} from "./bellatrix/index.js"; export {ts as capella} from "./capella/index.js"; export {ts as deneb} from "./deneb/index.js"; +export {ts as electra} from "./electra/index.js"; export {ts as allForks} from "./allForks/index.js"; diff --git a/packages/validator/src/util/params.ts b/packages/validator/src/util/params.ts index 8ccaf9fe75ba..0afede39b951 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -73,6 +73,7 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record Date: Tue, 20 Feb 2024 02:51:21 +0800 Subject: [PATCH 02/51] feat: implement EIP-6110 (#6042) * Add immutable in the dependencies * Initial change to pubkeyCache * Added todos * Moved unfinalized cache to epochCache * Move populating finalized cache to afterProcessEpoch * Specify unfinalized cache during state cloning * Move from unfinalized to finalized cache in afterProcessEpoch * Confused myself * Clean up * Change logic * Fix cloning issue * Clean up redundant code * Add CarryoverData in epochCtx.createFromState * Fix typo * Update usage of pubkeyCache * Update pubkeyCache usage * Fix lint * Fix lint * Add 6110 to ChainConfig * Add 6110 to BeaconPreset * Define 6110 fork and container * Add V6110 api to execution engine * Update test * Add depositReceiptsRoot to process_execution_payload * State transitioning to EIP6110 * State transitioning to EIP6110 * Light client change in EIP-6110 * Update tests * produceBlock * Refactor processDeposit to match the spec * Implement processDepositReceipt * Implement 6110 fork guard for pubkeyCache * Handle changes in eth1 deposit * Update eth1 deposit test * Fix typo * Lint * Remove embarassing comments * Address comments * Modify applyDeposit signature * Update packages/state-transition/src/cache/epochCache.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Update packages/state-transition/src/cache/epochCache.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Update packages/state-transition/src/cache/pubkeyCache.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Remove old code * Rename fields in epochCache and immutableData * Remove CarryoverData * Move isAfter6110 from var to method * Fix cyclic import * Fix operations spec runner * Fix for spec test * Fix spec test * state.depositReceiptsStartIndex to BigInt * getDeposit requires cached state * default depositReceiptsStartIndex value in genesis * Fix pubkeyCache bug * newUnfinalizedPubkeyIndexMap in createCachedBeaconState * Lint * Pass epochCache instead of pubkey2IndexFn in apis * Address comments * Add unit test on pubkey cache cloning * Add unfinalizedPubkeyCacheSize to metrics * Add unfinalizedPubkeyCacheSize to metrics * Clean up code * Add besu to el-interop * Add 6110 genesis file * Template for sim test * Add unit test for getEth1DepositCount * Update sim test * Update besudocker * Finish beacon api calls in sim test * Update epochCache.createFromState() * Fix bug unfinalized validators are not finalized * Add sim test to run a few blocks * Lint * Merge branch 'unstable' into 611 * Add more check to sim test * Update besu docker image instruction * Update sim test with correct tx * Address comment + cleanup * Clean up code * Properly handle promise rejection * Lint * Update packages/beacon-node/src/execution/engine/types.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Update comments * Accept type undefined in ExecutionPayloadBodyRpc * Update comment and semantic * Remove if statement when adding finalized validator * Comment on repeated insert on finalized cache * rename createFromState * Add comment on getPubkey() * Stash change to reduce diffs * Stash change to reduce diffs * Lint * addFinalizedPubkey on finalized checkpoint * Update comment * Use OrderedMap for unfinalized cache * Pull out logic of deleting pubkeys for batch op * Add updateUnfinalizedPubkeys in regen * Update updateUnfinalizedPubkeys logic * Add comment * Add metrics for state context caches * Address comment * Address comment * Deprecate eth1Data polling when condition is reached * Fix conflicts * Fix sim test * Lint * Fix type * Fix test * Fix test * Lint * Update packages/light-client/src/spec/utils.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Fix spec test * Address comments * Improve cache logic on checkpoint finalized * Update sim test according to new cache logic * Update comment * Lint * Finalized pubkey cache only update once per checkpoint * Add perf test for updateUnfinalizedPubkeys * Add perf test for updateUnfinalizedPubkeys * Tweak params for perf test * Freeze besu docker image version for 6110 * Add benchmark result * Use Map instead of OrderedMap. Update benchmark * Minor optimization * Minor optimization * Add memory test for immutable.js * Update test * Reduce code duplication * Lint * Remove try/catch in updateUnfinalizedPubkeys * Introduce EpochCache metric * Add historicalValidatorLengths * Polish code * Migrate state-transition unit tests to vitest * Fix calculation of pivot index * `historicalValidatorLengths` only activate post 6110 * Update sim test * Lint * Update packages/state-transition/src/cache/epochCache.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Improve readability on historicalValidatorLengths * Update types * Fix calculation * Add eth1data poll todo * Add epochCache.getValidatorCountAtEpoch * Add todo * Add getStateIterator for state cache * Partial commit * Update perf test * updateUnfinalizedPubkeys directly modify states from regen * Update sim test. Lint * Add todo * some improvements and a fix for effectiveBalanceIncrements fork safeness * rename eip6110 to elctra * fix electra-interop.test.ts --------- Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Co-authored-by: gajinder lint and tsc small cleanup fix rebase issue --- packages/beacon-node/src/chain/chain.ts | 37 ++ .../beacon-node/src/chain/regen/queued.ts | 50 +- .../chain/stateCache/blockStateCacheImpl.ts | 6 +- .../chain/stateCache/fifoBlockStateCache.ts | 4 + .../stateCache/inMemoryCheckpointsCache.ts | 4 + .../stateCache/persistentCheckpointsCache.ts | 4 + .../beacon-node/src/chain/stateCache/types.ts | 2 + .../src/eth1/eth1DepositDataTracker.ts | 32 +- packages/beacon-node/src/eth1/index.ts | 8 + packages/beacon-node/src/eth1/interface.ts | 5 + .../beacon-node/src/eth1/utils/deposits.ts | 14 +- .../beacon-node/src/execution/engine/http.ts | 10 +- .../src/execution/engine/interface.ts | 4 +- .../beacon-node/src/execution/engine/mock.ts | 4 + .../src/execution/engine/payloadIdCache.ts | 8 + .../beacon-node/src/execution/engine/types.ts | 68 ++- .../beacon-node/src/metrics/metrics/beacon.ts | 7 + .../src/metrics/metrics/lodestar.ts | 31 ++ .../test/memory/unfinalizedPubkey2Index.ts | 54 +++ .../opPools/aggregatedAttestationPool.test.ts | 2 +- .../test/perf/chain/opPools/opPool.test.ts | 2 +- .../produceBlock/produceBlockBody.test.ts | 2 +- .../updateUnfinalizedPubkeys.test.ts | 110 +++++ .../scripts/el-interop/besu/common-setup.sh | 19 + .../test/scripts/el-interop/besu/electra.tmpl | 77 +++ .../scripts/el-interop/besu/post-merge.sh | 8 + .../el-interop/besudocker/common-setup.sh | 22 + .../el-interop/besudocker/electra.tmpl | 77 +++ .../el-interop/besudocker/post-merge.sh | 8 + .../test/sim/electra-interop.test.ts | 457 ++++++++++++++++++ .../test/spec/presets/fork.test.ts | 3 +- .../test/spec/presets/genesis.test.ts | 4 +- .../test/spec/presets/operations.test.ts | 9 +- .../test/spec/presets/ssz_static.test.ts | 1 + .../test/spec/utils/specTestIterator.ts | 1 - .../upgradeLightClientHeader.test.ts | 34 +- .../opPools/aggregatedAttestationPool.test.ts | 4 +- .../test/unit/chain/shufflingCache.test.ts | 8 +- .../stateCache/fifoBlockStateCache.test.ts | 3 +- .../test/unit/eth1/utils/deposits.test.ts | 52 +- .../test/unit/executionEngine/http.test.ts | 4 + .../beaconBlocksMaybeBlobsByRange.test.ts | 1 + packages/beacon-node/test/utils/state.ts | 23 +- .../test/utils/validationData/attestation.ts | 7 +- .../config/src/chainConfig/configs/mainnet.ts | 2 +- .../config/src/chainConfig/configs/minimal.ts | 3 +- packages/light-client/src/spec/utils.ts | 14 +- packages/params/src/index.ts | 5 + packages/params/src/presets/mainnet.ts | 3 + packages/params/src/presets/minimal.ts | 3 + packages/params/src/types.ts | 6 + packages/state-transition/package.json | 7 +- .../src/block/processDeposit.ts | 112 +++-- .../src/block/processDepositReceipt.ts | 17 + .../src/block/processOperations.ts | 17 +- .../state-transition/src/cache/epochCache.ts | 184 ++++++- .../state-transition/src/cache/pubkeyCache.ts | 21 +- .../state-transition/src/cache/stateCache.ts | 5 +- packages/state-transition/src/cache/types.ts | 6 +- packages/state-transition/src/index.ts | 9 +- packages/state-transition/src/metrics.ts | 5 + .../src/signatureSets/attesterSlashings.ts | 3 +- packages/state-transition/src/slot/index.ts | 1 + .../src/slot/upgradeStateToElectra.ts | 33 ++ .../state-transition/src/stateTransition.ts | 5 + packages/state-transition/src/types.ts | 2 + packages/state-transition/src/util/deposit.ts | 24 + .../state-transition/src/util/execution.ts | 7 +- packages/state-transition/src/util/genesis.ts | 12 + packages/state-transition/src/util/index.ts | 1 + .../test/unit/cachedBeaconState.test.ts | 62 ++- .../test/unit/upgradeState.test.ts | 16 + .../test/unit/util/deposit.test.ts | 99 ++++ packages/types/package.json | 3 + packages/types/src/allForks/sszTypes.ts | 2 +- packages/types/src/allForks/types.ts | 170 +++++-- packages/types/src/electra/sszTypes.ts | 147 +++++- packages/types/src/electra/types.ts | 8 +- packages/types/src/primitive/sszTypes.ts | 1 + packages/validator/src/util/params.ts | 4 +- yarn.lock | 5 + 81 files changed, 2101 insertions(+), 213 deletions(-) create mode 100644 packages/beacon-node/test/memory/unfinalizedPubkey2Index.ts create mode 100644 packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts create mode 100755 packages/beacon-node/test/scripts/el-interop/besu/common-setup.sh create mode 100644 packages/beacon-node/test/scripts/el-interop/besu/electra.tmpl create mode 100755 packages/beacon-node/test/scripts/el-interop/besu/post-merge.sh create mode 100644 packages/beacon-node/test/scripts/el-interop/besudocker/common-setup.sh create mode 100644 packages/beacon-node/test/scripts/el-interop/besudocker/electra.tmpl create mode 100755 packages/beacon-node/test/scripts/el-interop/besudocker/post-merge.sh create mode 100644 packages/beacon-node/test/sim/electra-interop.test.ts create mode 100644 packages/state-transition/src/block/processDepositReceipt.ts create mode 100644 packages/state-transition/src/slot/upgradeStateToElectra.ts create mode 100644 packages/state-transition/src/util/deposit.ts create mode 100644 packages/state-transition/test/unit/util/deposit.test.ts diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 2f58962f3cc5..25b72c57f769 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -973,6 +973,9 @@ export class BeaconChain implements IBeaconChain { metrics.forkChoice.balancesLength.set(forkChoiceMetrics.balancesLength); metrics.forkChoice.nodes.set(forkChoiceMetrics.nodes); metrics.forkChoice.indices.set(forkChoiceMetrics.indices); + + const headState = this.getHeadState(); + metrics.headState.unfinalizedPubkeyCacheSize.set(headState.epochCtx.unfinalizedPubkey2index.size); } private onClockSlot(slot: Slot): void { @@ -1061,6 +1064,40 @@ export class BeaconChain implements IBeaconChain { if (headState) { this.opPool.pruneAll(headBlock, headState); } + + const cpEpoch = cp.epoch; + const electraEpoch = headState?.config.ELECTRA_FORK_EPOCH ?? Infinity; + + if (headState === null) { + this.logger.verbose("Head state is null"); + } else if (cpEpoch >= electraEpoch) { + // Get the validator.length from the state at cpEpoch + // We are confident the last element in the list is from headEpoch + // Thus we query from the end of the list. (cpEpoch - headEpoch - 1) is negative number + const pivotValidatorIndex = headState.epochCtx.getValidatorCountAtEpoch(cpEpoch); + + if (pivotValidatorIndex !== undefined) { + // Note EIP-6914 will break this logic + const newFinalizedValidators = headState.epochCtx.unfinalizedPubkey2index.filter( + (index, _pubkey) => index < pivotValidatorIndex + ); + + // Populate finalized pubkey cache and remove unfinalized pubkey cache + if (!newFinalizedValidators.isEmpty()) { + this.regen.updateUnfinalizedPubkeys(newFinalizedValidators); + } + } + } + + // TODO-Electra: Deprecating eth1Data poll requires a check on a finalized checkpoint state. + // Will resolve this later + // if (cpEpoch >= (this.config.ELECTRA_FORK_EPOCH ?? Infinity)) { + // // finalizedState can be safely casted to Electra state since cp is already post-Electra + // if (finalizedState.eth1DepositIndex >= (finalizedState as CachedBeaconStateElectra).depositReceiptsStartIndex) { + // // Signal eth1 to stop polling eth1Data + // this.eth1.stopPollingEth1Data(); + // } + // } } async updateBeaconProposerData(epoch: Epoch, proposers: ProposerPreparationData[]): Promise { diff --git a/packages/beacon-node/src/chain/regen/queued.ts b/packages/beacon-node/src/chain/regen/queued.ts index 148ec756497d..b988d98b8c55 100644 --- a/packages/beacon-node/src/chain/regen/queued.ts +++ b/packages/beacon-node/src/chain/regen/queued.ts @@ -1,7 +1,7 @@ import {toHexString} from "@chainsafe/ssz"; import {phase0, Slot, allForks, RootHex, Epoch} from "@lodestar/types"; import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; -import {CachedBeaconStateAllForks, computeEpochAtSlot} from "@lodestar/state-transition"; +import {CachedBeaconStateAllForks, UnfinalizedPubkeyIndexMap, computeEpochAtSlot} from "@lodestar/state-transition"; import {Logger} from "@lodestar/utils"; import {routes} from "@lodestar/api"; import {CheckpointHex, toCheckpointHex} from "../stateCache/index.js"; @@ -195,6 +195,54 @@ export class QueuedStateRegenerator implements IStateRegenerator { return this.checkpointStateCache.updatePreComputedCheckpoint(rootHex, epoch); } + /** + * Remove `validators` from all unfinalized cache's epochCtx.UnfinalizedPubkey2Index, + * and add them to epochCtx.pubkey2index and epochCtx.index2pubkey + */ + updateUnfinalizedPubkeys(validators: UnfinalizedPubkeyIndexMap): void { + let numStatesUpdated = 0; + const states = this.stateCache.getStates(); + const cpStates = this.checkpointStateCache.getStates(); + + // Add finalized pubkeys to all states. + const addTimer = this.metrics?.regenFnAddPubkeyTime.startTimer(); + + // We only need to add pubkeys to any one of the states since the finalized caches is shared globally across all states + const firstState = (states.next().value ?? cpStates.next().value) as CachedBeaconStateAllForks | undefined; + + if (firstState !== undefined) { + firstState.epochCtx.addFinalizedPubkeys(validators, this.metrics?.epochCache ?? undefined); + } else { + this.logger.warn("Attempt to delete finalized pubkey from unfinalized pubkey cache. But no state is available"); + } + + addTimer?.(); + + // Delete finalized pubkeys from unfinalized pubkey cache for all states + const deleteTimer = this.metrics?.regenFnDeletePubkeyTime.startTimer(); + const pubkeysToDelete = Array.from(validators.keys()); + + for (const s of states) { + s.epochCtx.deleteUnfinalizedPubkeys(pubkeysToDelete); + numStatesUpdated++; + } + + for (const s of cpStates) { + s.epochCtx.deleteUnfinalizedPubkeys(pubkeysToDelete); + numStatesUpdated++; + } + + // Since first state is consumed from the iterator. Will need to perform delete explicitly + if (firstState !== undefined) { + firstState?.epochCtx.deleteUnfinalizedPubkeys(pubkeysToDelete); + numStatesUpdated++; + } + + deleteTimer?.(); + + this.metrics?.regenFnNumStatesUpdated.observe(numStatesUpdated); + } + /** * Get the state to run with `block`. * - State after `block.parentRoot` dialed forward to block.slot diff --git a/packages/beacon-node/src/chain/stateCache/blockStateCacheImpl.ts b/packages/beacon-node/src/chain/stateCache/blockStateCacheImpl.ts index fdeb3ed5a659..1389235b161e 100644 --- a/packages/beacon-node/src/chain/stateCache/blockStateCacheImpl.ts +++ b/packages/beacon-node/src/chain/stateCache/blockStateCacheImpl.ts @@ -34,7 +34,7 @@ export class BlockStateCacheImpl implements BlockStateCache { this.maxStates = maxStates; this.cache = new MapTracker(metrics?.stateCache); if (metrics) { - this.metrics = metrics.stateCache; + this.metrics = {...metrics.stateCache, ...metrics.epochCache}; metrics.stateCache.size.addCollect(() => metrics.stateCache.size.set(this.cache.size)); } } @@ -137,6 +137,10 @@ export class BlockStateCacheImpl implements BlockStateCache { })); } + getStates(): IterableIterator { + return this.cache.values(); + } + private deleteAllEpochItems(epoch: Epoch): void { for (const rootHex of this.epochIndex.get(epoch) || []) { this.cache.delete(rootHex); diff --git a/packages/beacon-node/src/chain/stateCache/fifoBlockStateCache.ts b/packages/beacon-node/src/chain/stateCache/fifoBlockStateCache.ts index 93b581633c05..7498c8a02353 100644 --- a/packages/beacon-node/src/chain/stateCache/fifoBlockStateCache.ts +++ b/packages/beacon-node/src/chain/stateCache/fifoBlockStateCache.ts @@ -190,6 +190,10 @@ export class FIFOBlockStateCache implements BlockStateCache { })); } + getStates(): IterableIterator { + throw new Error("Method not implemented."); + } + /** * For unit test only. */ diff --git a/packages/beacon-node/src/chain/stateCache/inMemoryCheckpointsCache.ts b/packages/beacon-node/src/chain/stateCache/inMemoryCheckpointsCache.ts index 37c67c0d86b4..1bb45dfaa1a6 100644 --- a/packages/beacon-node/src/chain/stateCache/inMemoryCheckpointsCache.ts +++ b/packages/beacon-node/src/chain/stateCache/inMemoryCheckpointsCache.ts @@ -177,6 +177,10 @@ export class InMemoryCheckpointStateCache implements CheckpointStateCache { })); } + getStates(): IterableIterator { + return this.cache.values(); + } + /** ONLY FOR DEBUGGING PURPOSES. For spec tests on error */ dumpCheckpointKeys(): string[] { return Array.from(this.cache.keys()); diff --git a/packages/beacon-node/src/chain/stateCache/persistentCheckpointsCache.ts b/packages/beacon-node/src/chain/stateCache/persistentCheckpointsCache.ts index 58aeca061bc0..b3888641be66 100644 --- a/packages/beacon-node/src/chain/stateCache/persistentCheckpointsCache.ts +++ b/packages/beacon-node/src/chain/stateCache/persistentCheckpointsCache.ts @@ -594,6 +594,10 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { }); } + getStates(): IterableIterator { + throw new Error("Method not implemented."); + } + /** ONLY FOR DEBUGGING PURPOSES. For spec tests on error */ dumpCheckpointKeys(): string[] { return Array.from(this.cache.keys()); diff --git a/packages/beacon-node/src/chain/stateCache/types.ts b/packages/beacon-node/src/chain/stateCache/types.ts index 41e9b91aaa43..ad93def481e7 100644 --- a/packages/beacon-node/src/chain/stateCache/types.ts +++ b/packages/beacon-node/src/chain/stateCache/types.ts @@ -33,6 +33,7 @@ export interface BlockStateCache { prune(headStateRootHex: RootHex): void; deleteAllBeforeEpoch(finalizedEpoch: Epoch): void; dumpSummary(): routes.lodestar.StateCacheItem[]; + getStates(): IterableIterator; // Expose beacon states stored in cache. Use with caution } /** @@ -74,6 +75,7 @@ export interface CheckpointStateCache { processState(blockRootHex: RootHex, state: CachedBeaconStateAllForks): Promise; clear(): void; dumpSummary(): routes.lodestar.StateCacheItem[]; + getStates(): IterableIterator; // Expose beacon states stored in cache. Use with caution } export enum CacheItemType { diff --git a/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts b/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts index f36f70abbbc4..c0b3ab35a73a 100644 --- a/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts +++ b/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts @@ -1,6 +1,11 @@ import {phase0, ssz} from "@lodestar/types"; import {ChainForkConfig} from "@lodestar/config"; -import {BeaconStateAllForks, becomesNewEth1Data} from "@lodestar/state-transition"; +import { + BeaconStateAllForks, + CachedBeaconStateAllForks, + CachedBeaconStateElectra, + becomesNewEth1Data, +} from "@lodestar/state-transition"; import {ErrorAborted, TimeoutError, fromHex, Logger, isErrorAborted, sleep} from "@lodestar/utils"; import {IBeaconDb} from "../db/index.js"; @@ -67,6 +72,8 @@ export class Eth1DepositDataTracker { /** Dynamically adjusted batch size to fetch deposit logs */ private eth1GetLogsBatchSizeDynamic = MAX_BLOCKS_PER_LOG_QUERY; private readonly forcedEth1DataVote: phase0.Eth1Data | null; + /** To stop `runAutoUpdate()` in addition to AbortSignal */ + private stopPolling: boolean; constructor( opts: Eth1Options, @@ -81,6 +88,8 @@ export class Eth1DepositDataTracker { this.depositsCache = new Eth1DepositsCache(opts, config, db); this.eth1DataCache = new Eth1DataCache(config, db); this.eth1FollowDistance = config.ETH1_FOLLOW_DISTANCE; + // TODO Electra: fix scenario where node starts post-Electra and `stopPolling` will always be false + this.stopPolling = false; this.forcedEth1DataVote = opts.forcedEth1DataVote ? ssz.phase0.Eth1Data.deserialize(fromHex(opts.forcedEth1DataVote)) @@ -109,10 +118,22 @@ export class Eth1DepositDataTracker { } } + // TODO Electra: Figure out how an elegant way to stop eth1data polling + stopPollingEth1Data(): void { + this.stopPolling = true; + } + /** * Return eth1Data and deposits ready for block production for a given state */ - async getEth1DataAndDeposits(state: BeaconStateAllForks): Promise { + async getEth1DataAndDeposits(state: CachedBeaconStateAllForks): Promise { + if ( + state.epochCtx.isAfterElectra() && + state.eth1DepositIndex >= (state as CachedBeaconStateElectra).depositReceiptsStartIndex + ) { + // No need to poll eth1Data since Electra deprecates the mechanism after depositReceiptsStartIndex is reached + return {eth1Data: state.eth1Data, deposits: []}; + } const eth1Data = this.forcedEth1DataVote ?? (await this.getEth1Data(state)); const deposits = await this.getDeposits(state, eth1Data); return {eth1Data, deposits}; @@ -141,7 +162,10 @@ export class Eth1DepositDataTracker { * Returns deposits to be included for a given state and eth1Data vote. * Requires internal caches to be updated regularly to return good results */ - private async getDeposits(state: BeaconStateAllForks, eth1DataVote: phase0.Eth1Data): Promise { + private async getDeposits( + state: CachedBeaconStateAllForks, + eth1DataVote: phase0.Eth1Data + ): Promise { // No new deposits have to be included, continue if (eth1DataVote.depositCount === state.eth1DepositIndex) { return []; @@ -162,7 +186,7 @@ export class Eth1DepositDataTracker { private async runAutoUpdate(): Promise { let lastRunMs = 0; - while (!this.signal.aborted) { + while (!this.signal.aborted && !this.stopPolling) { lastRunMs = Date.now(); try { diff --git a/packages/beacon-node/src/eth1/index.ts b/packages/beacon-node/src/eth1/index.ts index 9fdba90258a2..a8ba55c54141 100644 --- a/packages/beacon-node/src/eth1/index.ts +++ b/packages/beacon-node/src/eth1/index.ts @@ -106,6 +106,10 @@ export class Eth1ForBlockProduction implements IEth1ForBlockProduction { startPollingMergeBlock(): void { return this.eth1MergeBlockTracker.startPollingMergeBlock(); } + + stopPollingEth1Data(): void { + return this.eth1DepositDataTracker?.stopPollingEth1Data(); + } } /** @@ -140,4 +144,8 @@ export class Eth1ForBlockProductionDisabled implements IEth1ForBlockProduction { startPollingMergeBlock(): void { // Ignore } + + stopPollingEth1Data(): void { + // Ignore + } } diff --git a/packages/beacon-node/src/eth1/interface.ts b/packages/beacon-node/src/eth1/interface.ts index fc9626eb5b8a..898041ac8947 100644 --- a/packages/beacon-node/src/eth1/interface.ts +++ b/packages/beacon-node/src/eth1/interface.ts @@ -62,6 +62,11 @@ export interface IEth1ForBlockProduction { * - head state not isMergeTransitionComplete */ startPollingMergeBlock(): void; + + /** + * Should stop polling eth1Data after a Electra block is finalized AND deposit_receipts_start_index is reached + */ + stopPollingEth1Data(): void; } /** Different Eth1Block from phase0.Eth1Block with blockHash */ diff --git a/packages/beacon-node/src/eth1/utils/deposits.ts b/packages/beacon-node/src/eth1/utils/deposits.ts index 19544917ffdc..7cf64d4455d2 100644 --- a/packages/beacon-node/src/eth1/utils/deposits.ts +++ b/packages/beacon-node/src/eth1/utils/deposits.ts @@ -1,9 +1,9 @@ import {toGindex, Tree} from "@chainsafe/persistent-merkle-tree"; import {toHexString} from "@chainsafe/ssz"; -import {MAX_DEPOSITS} from "@lodestar/params"; -import {BeaconStateAllForks} from "@lodestar/state-transition"; +import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; import {phase0, ssz} from "@lodestar/types"; import {FilterOptions} from "@lodestar/db"; +import {getEth1DepositCount} from "@lodestar/state-transition"; import {Eth1Error, Eth1ErrorCode} from "../errors.js"; import {DepositTree} from "../../db/repositories/depositDataRoot.js"; @@ -11,7 +11,7 @@ export type DepositGetter = (indexRange: FilterOptions, eth1Data: pha export async function getDeposits( // eth1_deposit_index represents the next deposit index to be added - state: BeaconStateAllForks, + state: CachedBeaconStateAllForks, eth1Data: phase0.Eth1Data, depositsGetter: DepositGetter ): Promise { @@ -22,9 +22,11 @@ export async function getDeposits( throw new Eth1Error({code: Eth1ErrorCode.DEPOSIT_INDEX_TOO_HIGH, depositIndex, depositCount}); } - // Spec v0.12.2 - // assert len(body.deposits) == min(MAX_DEPOSITS, state.eth1_data.deposit_count - state.eth1_deposit_index) - const depositsLen = Math.min(MAX_DEPOSITS, depositCount - depositIndex); + const depositsLen = getEth1DepositCount(state, eth1Data); + + if (depositsLen === 0) { + return []; // If depositsLen === 0, we can return early since no deposit with be returned from depositsGetter + } const indexRange = {gte: depositIndex, lt: depositIndex + depositsLen}; const deposits = await depositsGetter(indexRange, eth1Data); diff --git a/packages/beacon-node/src/execution/engine/http.ts b/packages/beacon-node/src/execution/engine/http.ts index f5ec03f41626..274e2164c7be 100644 --- a/packages/beacon-node/src/execution/engine/http.ts +++ b/packages/beacon-node/src/execution/engine/http.ts @@ -176,7 +176,9 @@ export class ExecutionEngineHttp implements IExecutionEngine { parentBlockRoot?: Root ): Promise { const method = - ForkSeq[fork] >= ForkSeq.deneb + ForkSeq[fork] >= ForkSeq.electra + ? "engine_newPayloadV6110" + : ForkSeq[fork] >= ForkSeq.deneb ? "engine_newPayloadV3" : ForkSeq[fork] >= ForkSeq.capella ? "engine_newPayloadV2" @@ -196,7 +198,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { const serializedVersionedHashes = serializeVersionedHashes(versionedHashes); const parentBeaconBlockRoot = serializeBeaconBlockRoot(parentBlockRoot); - const method = "engine_newPayloadV3"; + const method = ForkSeq[fork] >= ForkSeq.electra ? "engine_newPayloadV6110" : "engine_newPayloadV3"; engineRequest = { method, params: [serializedExecutionPayload, serializedVersionedHashes, parentBeaconBlockRoot], @@ -370,7 +372,9 @@ export class ExecutionEngineHttp implements IExecutionEngine { shouldOverrideBuilder?: boolean; }> { const method = - ForkSeq[fork] >= ForkSeq.deneb + ForkSeq[fork] >= ForkSeq.electra + ? "engine_getPayloadV6110" + : ForkSeq[fork] >= ForkSeq.deneb ? "engine_getPayloadV3" : ForkSeq[fork] >= ForkSeq.capella ? "engine_getPayloadV2" diff --git a/packages/beacon-node/src/execution/engine/interface.ts b/packages/beacon-node/src/execution/engine/interface.ts index e5f612fc0965..c303ee6d6bf3 100644 --- a/packages/beacon-node/src/execution/engine/interface.ts +++ b/packages/beacon-node/src/execution/engine/interface.ts @@ -3,10 +3,10 @@ import {KZGCommitment, Blob, KZGProof} from "@lodestar/types/deneb"; import {Root, RootHex, allForks, capella, Wei} from "@lodestar/types"; import {DATA} from "../../eth1/provider/utils.js"; -import {PayloadIdCache, PayloadId, WithdrawalV1} from "./payloadIdCache.js"; +import {PayloadIdCache, PayloadId, WithdrawalV1, DepositReceiptV1} from "./payloadIdCache.js"; import {ExecutionPayloadBody} from "./types.js"; -export {PayloadIdCache, type PayloadId, type WithdrawalV1}; +export {PayloadIdCache, type PayloadId, type WithdrawalV1, type DepositReceiptV1}; export enum ExecutionPayloadStatus { /** given payload is valid */ diff --git a/packages/beacon-node/src/execution/engine/mock.ts b/packages/beacon-node/src/execution/engine/mock.ts index 5779713435a5..83b63bc01c7c 100644 --- a/packages/beacon-node/src/execution/engine/mock.ts +++ b/packages/beacon-node/src/execution/engine/mock.ts @@ -35,6 +35,7 @@ export type ExecutionEngineMockOpts = { onlyPredefinedResponses?: boolean; capellaForkTimestamp?: number; denebForkTimestamp?: number; + electraForkTimestamp?: number; }; type ExecutionBlock = { @@ -88,12 +89,14 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { engine_newPayloadV1: this.notifyNewPayload.bind(this), engine_newPayloadV2: this.notifyNewPayload.bind(this), engine_newPayloadV3: this.notifyNewPayload.bind(this), + engine_newPayloadV6110: this.notifyNewPayload.bind(this), engine_forkchoiceUpdatedV1: this.notifyForkchoiceUpdate.bind(this), engine_forkchoiceUpdatedV2: this.notifyForkchoiceUpdate.bind(this), engine_forkchoiceUpdatedV3: this.notifyForkchoiceUpdate.bind(this), engine_getPayloadV1: this.getPayload.bind(this), engine_getPayloadV2: this.getPayload.bind(this), engine_getPayloadV3: this.getPayload.bind(this), + engine_getPayloadV6110: this.getPayload.bind(this), engine_getPayloadBodiesByHashV1: this.getPayloadBodiesByHash.bind(this), engine_getPayloadBodiesByRangeV1: this.getPayloadBodiesByRange.bind(this), }; @@ -387,6 +390,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { } private timestampToFork(timestamp: number): ForkExecution { + if (timestamp > (this.opts.electraForkTimestamp ?? Infinity)) return ForkName.electra; if (timestamp > (this.opts.denebForkTimestamp ?? Infinity)) return ForkName.deneb; if (timestamp > (this.opts.capellaForkTimestamp ?? Infinity)) return ForkName.capella; return ForkName.bellatrix; diff --git a/packages/beacon-node/src/execution/engine/payloadIdCache.ts b/packages/beacon-node/src/execution/engine/payloadIdCache.ts index ea37e0922e9c..e5baa9fba92d 100644 --- a/packages/beacon-node/src/execution/engine/payloadIdCache.ts +++ b/packages/beacon-node/src/execution/engine/payloadIdCache.ts @@ -18,6 +18,14 @@ export type WithdrawalV1 = { amount: QUANTITY; }; +export type DepositReceiptV1 = { + pubkey: DATA; + withdrawalCredentials: DATA; + amount: QUANTITY; + signature: DATA; + index: QUANTITY; +}; + 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 72a0100f7a51..ca64b20afaed 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -1,4 +1,4 @@ -import {allForks, capella, deneb, Wei, bellatrix, Root} from "@lodestar/types"; +import {allForks, capella, deneb, Wei, bellatrix, Root, electra} from "@lodestar/types"; import { BYTES_PER_LOGS_BLOOM, FIELD_ELEMENTS_PER_BLOB, @@ -17,7 +17,7 @@ import { quantityToBigint, } from "../../eth1/provider/utils.js"; import {ExecutionPayloadStatus, BlobsBundle, PayloadAttributes, VersionedHashes} from "./interface.js"; -import {WithdrawalV1} from "./payloadIdCache.js"; +import {WithdrawalV1, DepositReceiptV1} from "./payloadIdCache.js"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -28,6 +28,7 @@ export type EngineApiRpcParamTypes = { engine_newPayloadV1: [ExecutionPayloadRpc]; engine_newPayloadV2: [ExecutionPayloadRpc]; engine_newPayloadV3: [ExecutionPayloadRpc, VersionedHashesRpc, DATA]; + engine_newPayloadV6110: [ExecutionPayloadRpc, VersionedHashesRpc, DATA]; /** * 1. Object - Payload validity status with respect to the consensus rules: * - blockHash: DATA, 32 Bytes - block hash value of the payload @@ -51,6 +52,7 @@ export type EngineApiRpcParamTypes = { engine_getPayloadV1: [QUANTITY]; engine_getPayloadV2: [QUANTITY]; engine_getPayloadV3: [QUANTITY]; + engine_getPayloadV6110: [QUANTITY]; /** * 1. Array of DATA - Array of block_hash field values of the ExecutionPayload structure @@ -78,6 +80,7 @@ export type EngineApiRpcReturnTypes = { engine_newPayloadV1: PayloadStatus; engine_newPayloadV2: PayloadStatus; engine_newPayloadV3: PayloadStatus; + engine_newPayloadV6110: PayloadStatus; engine_forkchoiceUpdatedV1: { payloadStatus: PayloadStatus; payloadId: QUANTITY | null; @@ -96,6 +99,7 @@ export type EngineApiRpcReturnTypes = { engine_getPayloadV1: ExecutionPayloadRpc; engine_getPayloadV2: ExecutionPayloadResponse; engine_getPayloadV3: ExecutionPayloadResponse; + engine_getPayloadV6110: ExecutionPayloadResponse; engine_getPayloadBodiesByHashV1: (ExecutionPayloadBodyRpc | null)[]; @@ -111,9 +115,17 @@ type ExecutionPayloadRpcWithValue = { }; type ExecutionPayloadResponse = ExecutionPayloadRpc | ExecutionPayloadRpcWithValue; -export type ExecutionPayloadBodyRpc = {transactions: DATA[]; withdrawals: WithdrawalV1[] | null}; +export type ExecutionPayloadBodyRpc = { + transactions: DATA[]; + withdrawals: WithdrawalV1[] | null | undefined; + depositReceipts: DepositReceiptV1[] | null | undefined; +}; -export type ExecutionPayloadBody = {transactions: bellatrix.Transaction[]; withdrawals: capella.Withdrawals | null}; +export type ExecutionPayloadBody = { + transactions: bellatrix.Transaction[]; + withdrawals: capella.Withdrawals | null; + depositReceipts: electra.DepositReceipts | null; +}; export type ExecutionPayloadRpc = { parentHash: DATA; // 32 bytes @@ -134,6 +146,7 @@ export type ExecutionPayloadRpc = { blobGasUsed?: QUANTITY; // DENEB excessBlobGas?: QUANTITY; // DENEB parentBeaconBlockRoot?: QUANTITY; // DENEB + depositReceipts?: DepositReceiptRpc[]; // ELECTRA }; export type WithdrawalRpc = { @@ -143,6 +156,14 @@ export type WithdrawalRpc = { amount: QUANTITY; }; +export type DepositReceiptRpc = { + pubkey: DATA; + withdrawalCredentials: DATA; + amount: QUANTITY; + signature: DATA; + index: QUANTITY; +}; + export type VersionedHashesRpc = DATA[]; export type PayloadAttributesRpc = { @@ -194,6 +215,12 @@ export function serializeExecutionPayload(fork: ForkName, data: allForks.Executi payload.excessBlobGas = numToQuantity(excessBlobGas); } + // ELECTRA adds depositReceipts to the ExecutionPayload + if (ForkSeq[fork] >= ForkSeq.electra) { + const {depositReceipts} = data as electra.ExecutionPayload; + payload.depositReceipts = depositReceipts.map(serializeDepositReceipt); + } + return payload; } @@ -279,6 +306,17 @@ export function parseExecutionPayload( (executionPayload as deneb.ExecutionPayload).excessBlobGas = quantityToBigint(excessBlobGas); } + if (ForkSeq[fork] >= ForkSeq.electra) { + const {depositReceipts} = 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); + } + return {executionPayload, executionPayloadValue, blobsBundle, shouldOverrideBuilder}; } @@ -345,11 +383,32 @@ export function deserializeWithdrawal(serialized: WithdrawalRpc): capella.Withdr } as capella.Withdrawal; } +export function serializeDepositReceipt(depositReceipt: electra.DepositReceipt): DepositReceiptRpc { + return { + pubkey: bytesToData(depositReceipt.pubkey), + withdrawalCredentials: bytesToData(depositReceipt.withdrawalCredentials), + amount: numToQuantity(depositReceipt.amount), + signature: bytesToData(depositReceipt.signature), + index: numToQuantity(depositReceipt.index), + }; +} + +export function deserializeDepositReceipts(serialized: DepositReceiptRpc): electra.DepositReceipt { + return { + pubkey: dataToBytes(serialized.pubkey, 48), + withdrawalCredentials: dataToBytes(serialized.withdrawalCredentials, 32), + amount: quantityToNum(serialized.amount), + signature: dataToBytes(serialized.signature, 96), + index: quantityToNum(serialized.index), + } as electra.DepositReceipt; +} + 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, } : null; } @@ -359,6 +418,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, } : null; } diff --git a/packages/beacon-node/src/metrics/metrics/beacon.ts b/packages/beacon-node/src/metrics/metrics/beacon.ts index 141121de9079..96bb6bea4174 100644 --- a/packages/beacon-node/src/metrics/metrics/beacon.ts +++ b/packages/beacon-node/src/metrics/metrics/beacon.ts @@ -120,6 +120,13 @@ export function createBeaconMetrics(register: RegistryMetricCreator) { }), }, + headState: { + unfinalizedPubkeyCacheSize: register.gauge({ + name: "head_state_unfinalized_pubkey_cache_size", + help: "Current size of the unfinalizedPubkey2Index cache in the head state", + }), + }, + parentBlockDistance: register.histogram({ name: "beacon_imported_block_parent_distance", help: "Histogram of distance to parent block of valid imported blocks", diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index 12b7f3538d6e..cfa4145bfdcc 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -374,6 +374,17 @@ export function createLodestarMetrics( help: "Total count state.validators nodesPopulated is false on stfn for post state", }), + epochCache: { + finalizedPubkeyDuplicateInsert: register.gauge({ + name: "lodestar_epoch_cache_finalized_pubkey_duplicate_insert", + help: "Total count of duplicate insert of finalized pubkeys", + }), + newUnFinalizedPubkey: register.gauge({ + name: "lodestar_epoch_cache_new_unfinalized_pubkey", + help: "Total count of unfinalized pubkeys added", + }), + }, + // BLS verifier thread pool and queue bls: { @@ -1205,6 +1216,11 @@ export function createLodestarMetrics( help: "Histogram of time to serialize state to db", buckets: [0.1, 0.5, 1, 2, 3, 4], }), + numStatesUpdated: register.histogram({ + name: "lodestar_cp_state_cache_state_updated_count", + help: "Histogram of number of state cache items updated every time removing and adding pubkeys to pubkey cache", + buckets: [1, 2, 5, 10, 50, 250], + }), statePruneFromMemoryCount: register.gauge({ name: "lodestar_cp_state_cache_state_prune_from_memory_count", help: "Total number of states pruned from memory", @@ -1373,6 +1389,21 @@ export function createLodestarMetrics( help: "regen function total errors", labelNames: ["entrypoint", "caller"], }), + regenFnAddPubkeyTime: register.histogram({ + name: "lodestar_regen_fn_add_pubkey_time_seconds", + help: "Historgram of time spent on adding pubkeys to all state cache items in seconds", + buckets: [0.01, 0.1, 0.5, 1, 2, 5], + }), + regenFnDeletePubkeyTime: register.histogram({ + name: "lodestar_regen_fn_delete_pubkey_time_seconds", + help: "Histrogram of time spent on deleting pubkeys from all state cache items in seconds", + buckets: [0.01, 0.1, 0.5, 1, 2, 5], + }), + regenFnNumStatesUpdated: register.histogram({ + name: "lodestar_regen_state_cache_state_updated_count", + help: "Histogram of number of state cache items updated every time removing pubkeys from unfinalized cache", + buckets: [1, 2, 5, 10, 50, 250], + }), unhandledPromiseRejections: register.gauge({ name: "lodestar_unhandled_promise_rejections_total", help: "UnhandledPromiseRejection total count", diff --git a/packages/beacon-node/test/memory/unfinalizedPubkey2Index.ts b/packages/beacon-node/test/memory/unfinalizedPubkey2Index.ts new file mode 100644 index 000000000000..b37967d16ca4 --- /dev/null +++ b/packages/beacon-node/test/memory/unfinalizedPubkey2Index.ts @@ -0,0 +1,54 @@ +import crypto from "node:crypto"; +import {Map} from "immutable"; +import {ValidatorIndex} from "@lodestar/types"; +import {toMemoryEfficientHexStr} from "@lodestar/state-transition/src/cache/pubkeyCache.js"; +import {testRunnerMemory} from "./testRunnerMemory.js"; + +// Results in MacOS Nov 2023 +// +// UnfinalizedPubkey2Index 1000 keys - 274956.5 bytes / instance +// UnfinalizedPubkey2Index 10000 keys - 2591129.3 bytes / instance +// UnfinalizedPubkey2Index 100000 keys - 27261443.4 bytes / instance + +testRunnerMemoryBpi([ + { + id: "UnfinalizedPubkey2Index 1000 keys", + getInstance: () => getRandomMap(1000, () => toMemoryEfficientHexStr(crypto.randomBytes(48))), + }, + { + id: "UnfinalizedPubkey2Index 10000 keys", + getInstance: () => getRandomMap(10000, () => toMemoryEfficientHexStr(crypto.randomBytes(48))), + }, + { + id: "UnfinalizedPubkey2Index 100000 keys", + getInstance: () => getRandomMap(100000, () => toMemoryEfficientHexStr(crypto.randomBytes(48))), + }, +]); + +function getRandomMap(n: number, getKey: (i: number) => string): Map { + const map = Map(); + + return map.withMutations((m) => { + for (let i = 0; i < n; i++) { + m.set(getKey(i), i); + } + }); +} + +/** + * Test bytes per instance in different representations of raw binary data + */ +function testRunnerMemoryBpi(testCases: {getInstance: (bytes: number) => unknown; id: string}[]): void { + const longestId = Math.max(...testCases.map(({id}) => id.length)); + + for (const {id, getInstance} of testCases) { + const bpi = testRunnerMemory({ + getInstance, + convergeFactor: 1 / 100, + sampleEvery: 5, + }); + + // eslint-disable-next-line no-console + console.log(`${id.padEnd(longestId)} - ${bpi.toFixed(1)} bytes / instance`); + } +} 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 60ff6ce48302..7a882b03af0e 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}); + originalState = generatePerfTestCachedStateAltair({goBackOneSlot: true, vc}) as unknown 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 6e420f0e1011..7998a204f09d 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}); + originalState = generatePerfTestCachedStateAltair({goBackOneSlot: true}) as unknown 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 7bf8c2f7252f..2386f3538205 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(); + state = stateOg.clone() as unknown as CachedBeaconStateAltair; chain = new BeaconChain( { proposerBoost: true, diff --git a/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts b/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts new file mode 100644 index 000000000000..900f6a6fb873 --- /dev/null +++ b/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts @@ -0,0 +1,110 @@ +import {itBench, setBenchOpts} from "@dapplion/benchmark"; +import {Map} from "immutable"; +import {toBufferBE} from "bigint-buffer"; +import {digest} from "@chainsafe/as-sha256"; +import type {SecretKey} from "@chainsafe/bls/types"; +import bls from "@chainsafe/bls"; +import {ssz} from "@lodestar/types"; +import {type CachedBeaconStateAllForks, PubkeyIndexMap} from "@lodestar/state-transition"; +import {bytesToBigInt, intToBytes} from "@lodestar/utils"; +import {CheckpointStateCache, StateContextCache} from "../../../../src/chain/stateCache/index.js"; +import {generateCachedElectraState} from "../../../utils/state.js"; + +// Benchmark date from Mon Nov 21 2023 - Intel Core i7-9750H @ 2.60Ghz +// ✔ updateUnfinalizedPubkeys - updating 10 pubkeys 1444.173 ops/s 692.4380 us/op - 1057 runs 6.03 s +// ✔ updateUnfinalizedPubkeys - updating 100 pubkeys 189.5965 ops/s 5.274358 ms/op - 57 runs 1.15 s +// ✔ updateUnfinalizedPubkeys - updating 1000 pubkeys 12.90495 ops/s 77.48967 ms/op - 13 runs 1.62 s +describe("updateUnfinalizedPubkeys perf tests", function () { + setBenchOpts({noThreshold: true}); + + const numPubkeysToBeFinalizedCases = [10, 100, 1000]; + const numCheckpointStateCache = 8; + const numStateCache = 3 * 32; + + let checkpointStateCache: CheckpointStateCache; + let stateCache: StateContextCache; + + const unfinalizedPubkey2Index = generatePubkey2Index(0, Math.max.apply(null, numPubkeysToBeFinalizedCases)); + const baseState = generateCachedElectraState(); + + for (const numPubkeysToBeFinalized of numPubkeysToBeFinalizedCases) { + itBench({ + id: `updateUnfinalizedPubkeys - updating ${numPubkeysToBeFinalized} pubkeys`, + beforeEach: async () => { + baseState.epochCtx.unfinalizedPubkey2index = Map(unfinalizedPubkey2Index.map); + baseState.epochCtx.pubkey2index = new PubkeyIndexMap(); + baseState.epochCtx.index2pubkey = []; + + checkpointStateCache = new CheckpointStateCache({}); + stateCache = new StateContextCache({}); + + for (let i = 0; i < numCheckpointStateCache; i++) { + const clonedState = baseState.clone(); + const checkpoint = ssz.phase0.Checkpoint.defaultValue(); + + clonedState.slot = i; + checkpoint.epoch = i; // Assigning arbitrary non-duplicate values to ensure checkpointStateCache correctly saves all the states + + checkpointStateCache.add(checkpoint, clonedState); + } + + for (let i = 0; i < numStateCache; i++) { + const clonedState = baseState.clone(); + clonedState.slot = i; + stateCache.add(clonedState); + } + }, + fn: async () => { + const newFinalizedValidators = baseState.epochCtx.unfinalizedPubkey2index.filter( + (index, _pubkey) => index < numPubkeysToBeFinalized + ); + + const states = stateCache.getStates(); + const cpStates = checkpointStateCache.getStates(); + + const firstState = states.next().value as CachedBeaconStateAllForks; + firstState.epochCtx.addFinalizedPubkeys(newFinalizedValidators); + + const pubkeysToDelete = Array.from(newFinalizedValidators.keys()); + + firstState.epochCtx.deleteUnfinalizedPubkeys(pubkeysToDelete); + + for (const s of states) { + s.epochCtx.deleteUnfinalizedPubkeys(pubkeysToDelete); + } + + for (const s of cpStates) { + s.epochCtx.deleteUnfinalizedPubkeys(pubkeysToDelete); + } + }, + }); + } + + function generatePubkey2Index(startIndex: number, endIndex: number): PubkeyIndexMap { + const pubkey2Index = new PubkeyIndexMap(); + const pubkeys = generatePubkeys(endIndex - startIndex); + + for (let i = startIndex; i < endIndex; i++) { + pubkey2Index.set(pubkeys[i], i); + } + + return pubkey2Index; + } + + function generatePubkeys(validatorCount: number): Uint8Array[] { + const keys = []; + + for (let i = 0; i < validatorCount; i++) { + const sk = generatePrivateKey(i); + const pk = sk.toPublicKey().toBytes(); + keys.push(pk); + } + + return keys; + } + + function generatePrivateKey(index: number): SecretKey { + const secretKeyBytes = toBufferBE(bytesToBigInt(digest(intToBytes(index, 32))) % BigInt("38581184513"), 32); + return bls.SecretKey.fromBytes(secretKeyBytes); + } +}); diff --git a/packages/beacon-node/test/scripts/el-interop/besu/common-setup.sh b/packages/beacon-node/test/scripts/el-interop/besu/common-setup.sh new file mode 100755 index 000000000000..1444f6d3a479 --- /dev/null +++ b/packages/beacon-node/test/scripts/el-interop/besu/common-setup.sh @@ -0,0 +1,19 @@ +#!/bin/bash -x + +echo $TTD +echo $DATA_DIR +echo $EL_BINARY_DIR +echo $JWT_SECRET_HEX +echo $TEMPLATE_FILE + +echo $scriptDir +echo $currentDir + + +env TTD=$TTD envsubst < $scriptDir/$TEMPLATE_FILE > $DATA_DIR/genesis.json +echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json +echo "12345678" > $DATA_DIR/password.txt +pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" + +# echo a hex encoded 256 bit secret into a file +echo $JWT_SECRET_HEX> $DATA_DIR/jwtsecret \ No newline at end of file diff --git a/packages/beacon-node/test/scripts/el-interop/besu/electra.tmpl b/packages/beacon-node/test/scripts/el-interop/besu/electra.tmpl new file mode 100644 index 000000000000..7a63bfbe36d6 --- /dev/null +++ b/packages/beacon-node/test/scripts/el-interop/besu/electra.tmpl @@ -0,0 +1,77 @@ +{ + "config": { + "chainId":6110, + "homesteadBlock":0, + "eip150Block":0, + "eip155Block":0, + "eip158Block":0, + "byzantiumBlock":0, + "constantinopleBlock":0, + "petersburgBlock":0, + "istanbulBlock":0, + "muirGlacierBlock":0, + "berlinBlock":0, + "londonBlock":0, + "terminalTotalDifficulty":0, + "cancunTime":0, + "experimentalEipsTime":10, + "clique": { + "period": 5, + "epoch": 30000 + }, + "depositContractAddress": "0x4242424242424242424242424242424242424242" + }, + "nonce":"0x42", + "timestamp":"0x0", + "extraData":"0x0000000000000000000000000000000000000000000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit":"0x1C9C380", + "difficulty":"0x400000000", + "mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase":"0x0000000000000000000000000000000000000000", + "alloc":{ + "0xa4664C40AACeBD82A2Db79f0ea36C06Bc6A19Adb": { + "balance": "1000000000000000000000000000" + }, + "0x4242424242424242424242424242424242424242": { + "balance": "0", + "code": "0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a4578063621fd130146101ba578063c5f2892f14610244575b600080fd5b34801561005057600080fd5b506100906004803603602081101561006757600080fd5b50357fffffffff000000000000000000000000000000000000000000000000000000001661026b565b604080519115158252519081900360200190f35b6101b8600480360360808110156100ba57600080fd5b8101906020810181356401000000008111156100d557600080fd5b8201836020820111156100e757600080fd5b8035906020019184600183028401116401000000008311171561010957600080fd5b91939092909160208101903564010000000081111561012757600080fd5b82018360208201111561013957600080fd5b8035906020019184600183028401116401000000008311171561015b57600080fd5b91939092909160208101903564010000000081111561017957600080fd5b82018360208201111561018b57600080fd5b803590602001918460018302840111640100000000831117156101ad57600080fd5b919350915035610304565b005b3480156101c657600080fd5b506101cf6110b5565b6040805160208082528351818301528351919283929083019185019080838360005b838110156102095781810151838201526020016101f1565b50505050905090810190601f1680156102365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561025057600080fd5b506102596110c7565b60408051918252519081900360200190f35b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f01ffc9a70000000000000000000000000000000000000000000000000000000014806102fe57507fffffffff0000000000000000000000000000000000000000000000000000000082167f8564090700000000000000000000000000000000000000000000000000000000145b92915050565b6030861461035d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118056026913960400191505060405180910390fd5b602084146103b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603681526020018061179c6036913960400191505060405180910390fd5b6060821461040f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806118786029913960400191505060405180910390fd5b670de0b6b3a7640000341015610470576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118526026913960400191505060405180910390fd5b633b9aca003406156104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806117d26033913960400191505060405180910390fd5b633b9aca00340467ffffffffffffffff811115610535576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602781526020018061182b6027913960400191505060405180910390fd5b6060610540826114ba565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a6105756020546114ba565b6040805160a0808252810189905290819060208201908201606083016080840160c085018e8e80828437600083820152601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690910187810386528c815260200190508c8c808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690920188810386528c5181528c51602091820193918e019250908190849084905b83811015610648578181015183820152602001610630565b50505050905090810190601f1680156106755780820380516001836020036101000a031916815260200191505b5086810383528881526020018989808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169092018881038452895181528951602091820193918b019250908190849084905b838110156106ef5781810151838201526020016106d7565b50505050905090810190601f16801561071c5780820380516001836020036101000a031916815260200191505b509d505050505050505050505050505060405180910390a1600060028a8a600060801b604051602001808484808284377fffffffffffffffffffffffffffffffff0000000000000000000000000000000090941691909301908152604080517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0818403018152601090920190819052815191955093508392506020850191508083835b602083106107fc57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016107bf565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610859573d6000803e3d6000fd5b5050506040513d602081101561086e57600080fd5b5051905060006002806108846040848a8c6116fe565b6040516020018083838082843780830192505050925050506040516020818303038152906040526040518082805190602001908083835b602083106108f857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016108bb565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610955573d6000803e3d6000fd5b5050506040513d602081101561096a57600080fd5b5051600261097b896040818d6116fe565b60405160009060200180848480828437919091019283525050604080518083038152602092830191829052805190945090925082918401908083835b602083106109f457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016109b7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610a51573d6000803e3d6000fd5b5050506040513d6020811015610a6657600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610ada57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610a9d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610b37573d6000803e3d6000fd5b5050506040513d6020811015610b4c57600080fd5b50516040805160208101858152929350600092600292839287928f928f92018383808284378083019250505093505050506040516020818303038152906040526040518082805190602001908083835b60208310610bd957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610b9c565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610c36573d6000803e3d6000fd5b5050506040513d6020811015610c4b57600080fd5b50516040518651600291889160009188916020918201918291908601908083835b60208310610ca957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610c6c565b6001836020036101000a0380198251168184511680821785525050505050509050018367ffffffffffffffff191667ffffffffffffffff1916815260180182815260200193505050506040516020818303038152906040526040518082805190602001908083835b60208310610d4e57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610d11565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610dab573d6000803e3d6000fd5b5050506040513d6020811015610dc057600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610e3457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610df7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610e91573d6000803e3d6000fd5b5050506040513d6020811015610ea657600080fd5b50519050858114610f02576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260548152602001806117486054913960600191505060405180910390fd5b60205463ffffffff11610f60576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806117276021913960400191505060405180910390fd5b602080546001019081905560005b60208110156110a9578160011660011415610fa0578260008260208110610f9157fe5b0155506110ac95505050505050565b600260008260208110610faf57fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061102557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610fe8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015611082573d6000803e3d6000fd5b5050506040513d602081101561109757600080fd5b50519250600282049150600101610f6e565b50fe5b50505050505050565b60606110c26020546114ba565b905090565b6020546000908190815b60208110156112f05781600116600114156111e6576002600082602081106110f557fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061116b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161112e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156111c8573d6000803e3d6000fd5b5050506040513d60208110156111dd57600080fd5b505192506112e2565b600283602183602081106111f657fe5b015460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061126b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161122e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156112c8573d6000803e3d6000fd5b5050506040513d60208110156112dd57600080fd5b505192505b6002820491506001016110d1565b506002826112ff6020546114ba565b600060401b6040516020018084815260200183805190602001908083835b6020831061135a57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161131d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790527fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000095909516920191825250604080518083037ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8018152601890920190819052815191955093508392850191508083835b6020831061143f57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101611402565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa15801561149c573d6000803e3d6000fd5b5050506040513d60208110156114b157600080fd5b50519250505090565b60408051600880825281830190925260609160208201818036833701905050905060c082901b8060071a60f81b826000815181106114f457fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060061a60f81b8260018151811061153757fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060051a60f81b8260028151811061157a57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060041a60f81b826003815181106115bd57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060031a60f81b8260048151811061160057fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060021a60f81b8260058151811061164357fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060011a60f81b8260068151811061168657fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060001a60f81b826007815181106116c957fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535050919050565b6000808585111561170d578182fd5b83861115611719578182fd5b505082019391909203915056fe4465706f736974436f6e74726163743a206d65726b6c6520747265652066756c6c4465706f736974436f6e74726163743a207265636f6e7374727563746564204465706f7369744461746120646f6573206e6f74206d6174636820737570706c696564206465706f7369745f646174615f726f6f744465706f736974436f6e74726163743a20696e76616c6964207769746864726177616c5f63726564656e7469616c73206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c7565206e6f74206d756c7469706c65206f6620677765694465706f736974436f6e74726163743a20696e76616c6964207075626b6579206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f20686967684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f206c6f774465706f736974436f6e74726163743a20696e76616c6964207369676e6174757265206c656e677468a26469706673582212201dd26f37a621703009abf16e77e69c93dc50c79db7f6cc37543e3e0e3decdc9764736f6c634300060b0033", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000022": "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0x0000000000000000000000000000000000000000000000000000000000000023": "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0x0000000000000000000000000000000000000000000000000000000000000024": "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "0x0000000000000000000000000000000000000000000000000000000000000025": "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", + "0x0000000000000000000000000000000000000000000000000000000000000026": "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", + "0x0000000000000000000000000000000000000000000000000000000000000027": "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0x0000000000000000000000000000000000000000000000000000000000000028": "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c", + "0x0000000000000000000000000000000000000000000000000000000000000029": "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x000000000000000000000000000000000000000000000000000000000000002a": "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0x000000000000000000000000000000000000000000000000000000000000002b": "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x000000000000000000000000000000000000000000000000000000000000002c": "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0x000000000000000000000000000000000000000000000000000000000000002d": "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0x000000000000000000000000000000000000000000000000000000000000002e": "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", + "0x000000000000000000000000000000000000000000000000000000000000002f": "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", + "0x0000000000000000000000000000000000000000000000000000000000000030": "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "0x0000000000000000000000000000000000000000000000000000000000000031": "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", + "0x0000000000000000000000000000000000000000000000000000000000000032": "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", + "0x0000000000000000000000000000000000000000000000000000000000000033": "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0x0000000000000000000000000000000000000000000000000000000000000034": "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0x0000000000000000000000000000000000000000000000000000000000000035": "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x0000000000000000000000000000000000000000000000000000000000000036": "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0x0000000000000000000000000000000000000000000000000000000000000037": "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0x0000000000000000000000000000000000000000000000000000000000000038": "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x0000000000000000000000000000000000000000000000000000000000000039": "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x000000000000000000000000000000000000000000000000000000000000003a": "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x000000000000000000000000000000000000000000000000000000000000003b": "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x000000000000000000000000000000000000000000000000000000000000003c": "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x000000000000000000000000000000000000000000000000000000000000003d": "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x000000000000000000000000000000000000000000000000000000000000003e": "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0x000000000000000000000000000000000000000000000000000000000000003f": "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x0000000000000000000000000000000000000000000000000000000000000040": "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7" + } + } + }, + "number":"0x0", + "gasUsed":"0x0", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas":"0x7" +} diff --git a/packages/beacon-node/test/scripts/el-interop/besu/post-merge.sh b/packages/beacon-node/test/scripts/el-interop/besu/post-merge.sh new file mode 100755 index 000000000000..47bec71cf8bb --- /dev/null +++ b/packages/beacon-node/test/scripts/el-interop/besu/post-merge.sh @@ -0,0 +1,8 @@ +#!/bin/bash -x + +scriptDir=$(dirname $0) +currentDir=$(pwd) + +. $scriptDir/common-setup.sh + +$EL_BINARY_DIR/besu --engine-rpc-enabled --rpc-http-enabled --rpc-http-api ADMIN,ETH,MINER,NET --rpc-http-port $ETH_PORT --engine-rpc-port $ENGINE_PORT --engine-jwt-secret $currentDir/$DATA_DIR/jwtsecret --data-path $DATA_DIR --data-storage-format BONSAI --genesis-file $DATA_DIR/genesis.json \ No newline at end of file diff --git a/packages/beacon-node/test/scripts/el-interop/besudocker/common-setup.sh b/packages/beacon-node/test/scripts/el-interop/besudocker/common-setup.sh new file mode 100644 index 000000000000..b3d93190ef2d --- /dev/null +++ b/packages/beacon-node/test/scripts/el-interop/besudocker/common-setup.sh @@ -0,0 +1,22 @@ +#!/bin/bash -x + +echo $TTD +echo $DATA_DIR +echo $EL_BINARY_DIR +echo $JWT_SECRET_HEX +echo $TEMPLATE_FILE + +echo $scriptDir +echo $currentDir + + +env TTD=$TTD envsubst < $scriptDir/$TEMPLATE_FILE > $DATA_DIR/genesis.json +echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json +echo "12345678" > $DATA_DIR/password.txt +pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" + +# echo a hex encoded 256 bit secret into a file +echo $JWT_SECRET_HEX> $DATA_DIR/jwtsecret +# clear any previous docker dangling docker run +docker rm -f custom-execution +rm -rf $DATA_DIR/besu diff --git a/packages/beacon-node/test/scripts/el-interop/besudocker/electra.tmpl b/packages/beacon-node/test/scripts/el-interop/besudocker/electra.tmpl new file mode 100644 index 000000000000..7a63bfbe36d6 --- /dev/null +++ b/packages/beacon-node/test/scripts/el-interop/besudocker/electra.tmpl @@ -0,0 +1,77 @@ +{ + "config": { + "chainId":6110, + "homesteadBlock":0, + "eip150Block":0, + "eip155Block":0, + "eip158Block":0, + "byzantiumBlock":0, + "constantinopleBlock":0, + "petersburgBlock":0, + "istanbulBlock":0, + "muirGlacierBlock":0, + "berlinBlock":0, + "londonBlock":0, + "terminalTotalDifficulty":0, + "cancunTime":0, + "experimentalEipsTime":10, + "clique": { + "period": 5, + "epoch": 30000 + }, + "depositContractAddress": "0x4242424242424242424242424242424242424242" + }, + "nonce":"0x42", + "timestamp":"0x0", + "extraData":"0x0000000000000000000000000000000000000000000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit":"0x1C9C380", + "difficulty":"0x400000000", + "mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase":"0x0000000000000000000000000000000000000000", + "alloc":{ + "0xa4664C40AACeBD82A2Db79f0ea36C06Bc6A19Adb": { + "balance": "1000000000000000000000000000" + }, + "0x4242424242424242424242424242424242424242": { + "balance": "0", + "code": "0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a4578063621fd130146101ba578063c5f2892f14610244575b600080fd5b34801561005057600080fd5b506100906004803603602081101561006757600080fd5b50357fffffffff000000000000000000000000000000000000000000000000000000001661026b565b604080519115158252519081900360200190f35b6101b8600480360360808110156100ba57600080fd5b8101906020810181356401000000008111156100d557600080fd5b8201836020820111156100e757600080fd5b8035906020019184600183028401116401000000008311171561010957600080fd5b91939092909160208101903564010000000081111561012757600080fd5b82018360208201111561013957600080fd5b8035906020019184600183028401116401000000008311171561015b57600080fd5b91939092909160208101903564010000000081111561017957600080fd5b82018360208201111561018b57600080fd5b803590602001918460018302840111640100000000831117156101ad57600080fd5b919350915035610304565b005b3480156101c657600080fd5b506101cf6110b5565b6040805160208082528351818301528351919283929083019185019080838360005b838110156102095781810151838201526020016101f1565b50505050905090810190601f1680156102365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561025057600080fd5b506102596110c7565b60408051918252519081900360200190f35b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f01ffc9a70000000000000000000000000000000000000000000000000000000014806102fe57507fffffffff0000000000000000000000000000000000000000000000000000000082167f8564090700000000000000000000000000000000000000000000000000000000145b92915050565b6030861461035d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118056026913960400191505060405180910390fd5b602084146103b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603681526020018061179c6036913960400191505060405180910390fd5b6060821461040f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806118786029913960400191505060405180910390fd5b670de0b6b3a7640000341015610470576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118526026913960400191505060405180910390fd5b633b9aca003406156104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806117d26033913960400191505060405180910390fd5b633b9aca00340467ffffffffffffffff811115610535576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602781526020018061182b6027913960400191505060405180910390fd5b6060610540826114ba565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a6105756020546114ba565b6040805160a0808252810189905290819060208201908201606083016080840160c085018e8e80828437600083820152601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690910187810386528c815260200190508c8c808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690920188810386528c5181528c51602091820193918e019250908190849084905b83811015610648578181015183820152602001610630565b50505050905090810190601f1680156106755780820380516001836020036101000a031916815260200191505b5086810383528881526020018989808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169092018881038452895181528951602091820193918b019250908190849084905b838110156106ef5781810151838201526020016106d7565b50505050905090810190601f16801561071c5780820380516001836020036101000a031916815260200191505b509d505050505050505050505050505060405180910390a1600060028a8a600060801b604051602001808484808284377fffffffffffffffffffffffffffffffff0000000000000000000000000000000090941691909301908152604080517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0818403018152601090920190819052815191955093508392506020850191508083835b602083106107fc57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016107bf565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610859573d6000803e3d6000fd5b5050506040513d602081101561086e57600080fd5b5051905060006002806108846040848a8c6116fe565b6040516020018083838082843780830192505050925050506040516020818303038152906040526040518082805190602001908083835b602083106108f857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016108bb565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610955573d6000803e3d6000fd5b5050506040513d602081101561096a57600080fd5b5051600261097b896040818d6116fe565b60405160009060200180848480828437919091019283525050604080518083038152602092830191829052805190945090925082918401908083835b602083106109f457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016109b7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610a51573d6000803e3d6000fd5b5050506040513d6020811015610a6657600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610ada57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610a9d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610b37573d6000803e3d6000fd5b5050506040513d6020811015610b4c57600080fd5b50516040805160208101858152929350600092600292839287928f928f92018383808284378083019250505093505050506040516020818303038152906040526040518082805190602001908083835b60208310610bd957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610b9c565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610c36573d6000803e3d6000fd5b5050506040513d6020811015610c4b57600080fd5b50516040518651600291889160009188916020918201918291908601908083835b60208310610ca957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610c6c565b6001836020036101000a0380198251168184511680821785525050505050509050018367ffffffffffffffff191667ffffffffffffffff1916815260180182815260200193505050506040516020818303038152906040526040518082805190602001908083835b60208310610d4e57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610d11565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610dab573d6000803e3d6000fd5b5050506040513d6020811015610dc057600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610e3457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610df7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610e91573d6000803e3d6000fd5b5050506040513d6020811015610ea657600080fd5b50519050858114610f02576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260548152602001806117486054913960600191505060405180910390fd5b60205463ffffffff11610f60576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806117276021913960400191505060405180910390fd5b602080546001019081905560005b60208110156110a9578160011660011415610fa0578260008260208110610f9157fe5b0155506110ac95505050505050565b600260008260208110610faf57fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061102557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610fe8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015611082573d6000803e3d6000fd5b5050506040513d602081101561109757600080fd5b50519250600282049150600101610f6e565b50fe5b50505050505050565b60606110c26020546114ba565b905090565b6020546000908190815b60208110156112f05781600116600114156111e6576002600082602081106110f557fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061116b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161112e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156111c8573d6000803e3d6000fd5b5050506040513d60208110156111dd57600080fd5b505192506112e2565b600283602183602081106111f657fe5b015460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061126b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161122e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156112c8573d6000803e3d6000fd5b5050506040513d60208110156112dd57600080fd5b505192505b6002820491506001016110d1565b506002826112ff6020546114ba565b600060401b6040516020018084815260200183805190602001908083835b6020831061135a57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161131d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790527fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000095909516920191825250604080518083037ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8018152601890920190819052815191955093508392850191508083835b6020831061143f57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101611402565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa15801561149c573d6000803e3d6000fd5b5050506040513d60208110156114b157600080fd5b50519250505090565b60408051600880825281830190925260609160208201818036833701905050905060c082901b8060071a60f81b826000815181106114f457fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060061a60f81b8260018151811061153757fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060051a60f81b8260028151811061157a57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060041a60f81b826003815181106115bd57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060031a60f81b8260048151811061160057fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060021a60f81b8260058151811061164357fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060011a60f81b8260068151811061168657fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060001a60f81b826007815181106116c957fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535050919050565b6000808585111561170d578182fd5b83861115611719578182fd5b505082019391909203915056fe4465706f736974436f6e74726163743a206d65726b6c6520747265652066756c6c4465706f736974436f6e74726163743a207265636f6e7374727563746564204465706f7369744461746120646f6573206e6f74206d6174636820737570706c696564206465706f7369745f646174615f726f6f744465706f736974436f6e74726163743a20696e76616c6964207769746864726177616c5f63726564656e7469616c73206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c7565206e6f74206d756c7469706c65206f6620677765694465706f736974436f6e74726163743a20696e76616c6964207075626b6579206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f20686967684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f206c6f774465706f736974436f6e74726163743a20696e76616c6964207369676e6174757265206c656e677468a26469706673582212201dd26f37a621703009abf16e77e69c93dc50c79db7f6cc37543e3e0e3decdc9764736f6c634300060b0033", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000022": "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0x0000000000000000000000000000000000000000000000000000000000000023": "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0x0000000000000000000000000000000000000000000000000000000000000024": "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "0x0000000000000000000000000000000000000000000000000000000000000025": "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", + "0x0000000000000000000000000000000000000000000000000000000000000026": "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", + "0x0000000000000000000000000000000000000000000000000000000000000027": "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0x0000000000000000000000000000000000000000000000000000000000000028": "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c", + "0x0000000000000000000000000000000000000000000000000000000000000029": "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x000000000000000000000000000000000000000000000000000000000000002a": "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0x000000000000000000000000000000000000000000000000000000000000002b": "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x000000000000000000000000000000000000000000000000000000000000002c": "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0x000000000000000000000000000000000000000000000000000000000000002d": "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0x000000000000000000000000000000000000000000000000000000000000002e": "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", + "0x000000000000000000000000000000000000000000000000000000000000002f": "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", + "0x0000000000000000000000000000000000000000000000000000000000000030": "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "0x0000000000000000000000000000000000000000000000000000000000000031": "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", + "0x0000000000000000000000000000000000000000000000000000000000000032": "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", + "0x0000000000000000000000000000000000000000000000000000000000000033": "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0x0000000000000000000000000000000000000000000000000000000000000034": "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0x0000000000000000000000000000000000000000000000000000000000000035": "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x0000000000000000000000000000000000000000000000000000000000000036": "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0x0000000000000000000000000000000000000000000000000000000000000037": "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0x0000000000000000000000000000000000000000000000000000000000000038": "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x0000000000000000000000000000000000000000000000000000000000000039": "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x000000000000000000000000000000000000000000000000000000000000003a": "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x000000000000000000000000000000000000000000000000000000000000003b": "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x000000000000000000000000000000000000000000000000000000000000003c": "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x000000000000000000000000000000000000000000000000000000000000003d": "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x000000000000000000000000000000000000000000000000000000000000003e": "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0x000000000000000000000000000000000000000000000000000000000000003f": "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x0000000000000000000000000000000000000000000000000000000000000040": "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7" + } + } + }, + "number":"0x0", + "gasUsed":"0x0", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas":"0x7" +} diff --git a/packages/beacon-node/test/scripts/el-interop/besudocker/post-merge.sh b/packages/beacon-node/test/scripts/el-interop/besudocker/post-merge.sh new file mode 100755 index 000000000000..fb091a7d838b --- /dev/null +++ b/packages/beacon-node/test/scripts/el-interop/besudocker/post-merge.sh @@ -0,0 +1,8 @@ +#!/bin/bash -x + +scriptDir=$(dirname $0) +currentDir=$(pwd) + +. $scriptDir/common-setup.sh + +docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --name custom-execution -p $ETH_PORT:$ETH_PORT -p $ENGINE_PORT:$ENGINE_PORT -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR --engine-rpc-enabled --rpc-http-enabled --rpc-http-api ADMIN,ETH,MINER,NET --rpc-http-port $ETH_PORT --engine-rpc-port $ENGINE_PORT --engine-jwt-secret /data/jwtsecret --data-path /data/besu --data-storage-format BONSAI --genesis-file /data/genesis.json \ No newline at end of file diff --git a/packages/beacon-node/test/sim/electra-interop.test.ts b/packages/beacon-node/test/sim/electra-interop.test.ts new file mode 100644 index 000000000000..c1663211cbd9 --- /dev/null +++ b/packages/beacon-node/test/sim/electra-interop.test.ts @@ -0,0 +1,457 @@ +import fs from "node:fs"; +import {describe, it, vi, afterAll, afterEach} from "vitest"; +/* eslint-disable @typescript-eslint/naming-convention */ +import _ from "lodash"; +import {LogLevel, sleep} from "@lodestar/utils"; +import {ForkName, SLOTS_PER_EPOCH, UNSET_DEPOSIT_RECEIPTS_START_INDEX} from "@lodestar/params"; +import {electra, Epoch, Slot} from "@lodestar/types"; +import {ValidatorProposerConfig} from "@lodestar/validator"; + +import {ChainConfig} from "@lodestar/config"; +import {TimestampFormatCode} from "@lodestar/logger"; +import {CachedBeaconStateElectra} from "@lodestar/state-transition"; +import {initializeExecutionEngine} from "../../src/execution/index.js"; +import {ExecutionPayloadStatus, PayloadAttributes} from "../../src/execution/engine/interface.js"; + +import {testLogger, TestLoggerOpts} from "../utils/logger.js"; +import {runEL, ELStartMode, ELClient, sendRawTransactionBig} from "../utils/runEl.js"; +import {defaultExecutionEngineHttpOpts} from "../../src/execution/engine/http.js"; +import {getDevBeaconNode} from "../utils/node/beacon.js"; +import {BeaconRestApiServerOpts} from "../../src/api/index.js"; +import {simTestInfoTracker} from "../utils/node/simTest.js"; +import {getAndInitDevValidators} from "../utils/node/validator.js"; +import {ClockEvent} from "../../src/util/clock.js"; +import {dataToBytes} from "../../src/eth1/provider/utils.js"; +import {bytesToData} from "../../lib/eth1/provider/utils.js"; +import {BeaconNode} from "../../src/index.js"; +import {logFilesDir} from "./params.js"; +import {shell} from "./shell.js"; + +// NOTE: How to run +// DEV_RUN=true EL_BINARY_DIR=naviechan/besu:v6110 EL_SCRIPT_DIR=besudocker yarn vitest --run test/sim/electra-interop.test.ts +// or +// DEV_RUN=true EL_BINARY_DIR=/Volumes/fast_boi/navie/Documents/workspace/besu/build/install/besu/bin EL_SCRIPT_DIR=besu yarn vitest --run test/sim/electra-interop.test.ts +// ``` + +/* eslint-disable no-console, @typescript-eslint/naming-convention */ + +const jwtSecretHex = "0xdc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d"; +const retries = defaultExecutionEngineHttpOpts.retries; +const retryDelay = defaultExecutionEngineHttpOpts.retryDelay; +describe("executionEngine / ExecutionEngineHttp", function () { + if (!process.env.EL_BINARY_DIR || !process.env.EL_SCRIPT_DIR) { + throw Error( + `EL ENV must be provided, EL_BINARY_DIR: ${process.env.EL_BINARY_DIR}, EL_SCRIPT_DIR: ${process.env.EL_SCRIPT_DIR}` + ); + } + vi.setConfig({testTimeout: 1000 * 60 * 10, hookTimeout: 1000 * 60 * 10}); + + const dataPath = fs.mkdtempSync("lodestar-test-electra"); + const elSetupConfig = { + elScriptDir: process.env.EL_SCRIPT_DIR, + elBinaryDir: process.env.EL_BINARY_DIR, + }; + const elRunOptions = { + dataPath, + jwtSecretHex, + enginePort: parseInt(process.env.ENGINE_PORT ?? "8551"), + ethPort: parseInt(process.env.ETH_PORT ?? "8545"), + }; + + const controller = new AbortController(); + afterAll(async () => { + controller?.abort(); + await shell(`sudo rm -rf ${dataPath}`); + }); + + const afterEachCallbacks: (() => Promise | void)[] = []; + afterEach(async () => { + while (afterEachCallbacks.length > 0) { + const callback = afterEachCallbacks.pop(); + if (callback) await callback(); + } + }); + + it("Send and get payloads with depositReceipts to/from EL", async () => { + const {elClient, tearDownCallBack} = await runEL( + {...elSetupConfig, mode: ELStartMode.PostMerge, genesisTemplate: "electra.tmpl"}, + {...elRunOptions, ttd: BigInt(0)}, + controller.signal + ); + afterEachCallbacks.push(() => tearDownCallBack()); + const {genesisBlockHash, engineRpcUrl, ethRpcUrl} = elClient; + console.log({genesisBlockHash}); + + const loggerExecutionEngine = testLogger("executionEngine"); + + const executionEngine = initializeExecutionEngine( + {mode: "http", urls: [engineRpcUrl], jwtSecretHex, retries, retryDelay}, + {signal: controller.signal, logger: loggerExecutionEngine} + ); + + // 1. Prepare payload + const preparePayloadParams: PayloadAttributes = { + // Note: this is created with a pre-defined genesis.json + timestamp: 10, + prevRandao: dataToBytes("0x0000000000000000000000000000000000000000000000000000000000000000", 32), + suggestedFeeRecipient: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + withdrawals: [], + parentBeaconBlockRoot: dataToBytes("0x0000000000000000000000000000000000000000000000000000000000000000", 32), + }; + const payloadId = await executionEngine.notifyForkchoiceUpdate( + ForkName.electra, + genesisBlockHash, + //use finalizedBlockHash as safeBlockHash + genesisBlockHash, + genesisBlockHash, + preparePayloadParams + ); + if (!payloadId) throw Error("InvalidPayloadId"); + + // 2. Send raw deposit transaction A and B. tx A is to be imported via newPayload, tx B is to be included in payload via getPayload + const depositTransactionA = + "0x02f9021c8217de808459682f008459682f0e830271009442424242424242424242424242424242424242428901bc16d674ec800000b901a422895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120749715de5d1226545c6b3790f515d551a5cc5bf1d49c87a696860554d2fc4f14000000000000000000000000000000000000000000000000000000000000003096a96086cff07df17668f35f7418ef8798079167e3f4f9b72ecde17b28226137cf454ab1dd20ef5d924786ab3483c2f9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020003f5102dabe0a27b1746098d1dc17a5d3fbd478759fea9287e4e419b3c3cef20000000000000000000000000000000000000000000000000000000000000060b1acdb2c4d3df3f1b8d3bfd33421660df358d84d78d16c4603551935f4b67643373e7eb63dcb16ec359be0ec41fee33b03a16e80745f2374ff1d3c352508ac5d857c6476d3c3bcf7e6ca37427c9209f17be3af5264c0e2132b3dd1156c28b4e9c080a09f597089338d7f44f5c59f8230bb38f243849228a8d4e9d2e2956e6050f5b2c7a076486996c7e62802b8f95eee114783e4b403fd11093ba96286ff42c595f24452"; + const depositReceiptA = { + amount: 32000000000, + index: 0, + pubkey: dataToBytes( + "0x96a96086cff07df17668f35f7418ef8798079167e3f4f9b72ecde17b28226137cf454ab1dd20ef5d924786ab3483c2f9", + 48 + ), + signature: dataToBytes( + "0xb1acdb2c4d3df3f1b8d3bfd33421660df358d84d78d16c4603551935f4b67643373e7eb63dcb16ec359be0ec41fee33b03a16e80745f2374ff1d3c352508ac5d857c6476d3c3bcf7e6ca37427c9209f17be3af5264c0e2132b3dd1156c28b4e9", + 96 + ), + withdrawalCredentials: dataToBytes("0x003f5102dabe0a27b1746098d1dc17a5d3fbd478759fea9287e4e419b3c3cef2", 32), + }; + + const depositTransactionB = + "0x02f9021c8217de018459682f008459682f0e830271009442424242424242424242424242424242424242428901bc16d674ec800000b901a422895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120a18b4c7cab0afa273ea9504904521ea8421a4e32740b7611bd3d5095ca99f0cb0000000000000000000000000000000000000000000000000000000000000030a5c85a60ba2905c215f6a12872e62b1ee037051364244043a5f639aa81b04a204c55e7cc851f29c7c183be253ea1510b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020001db70c485b6264692f26b8aeaab5b0c384180df8e2184a21a808a3ec8e86ca00000000000000000000000000000000000000000000000000000000000000609561731785b48cf1886412234531e4940064584463e96ac63a1a154320227e333fb51addc4a89b7e0d3f862d7c1fd4ea03bd8eb3d8806f1e7daf591cbbbb92b0beb74d13c01617f22c5026b4f9f9f294a8a7c32db895de3b01bee0132c9209e1c001a0644e0a763a34b4bfb9f56a677857b57fcf15e3db57e2f57060e92084f75f3d82a018ba8eaacbd8e6f6917675b1d0362b12ca82850ca8ef9c010430760c2b2e0cb5"; + const depositReceiptB = { + amount: 32000000000, + index: 1, + pubkey: dataToBytes( + "0xa5c85a60ba2905c215f6a12872e62b1ee037051364244043a5f639aa81b04a204c55e7cc851f29c7c183be253ea1510b", + 48 + ), + signature: dataToBytes( + "0x9561731785b48cf1886412234531e4940064584463e96ac63a1a154320227e333fb51addc4a89b7e0d3f862d7c1fd4ea03bd8eb3d8806f1e7daf591cbbbb92b0beb74d13c01617f22c5026b4f9f9f294a8a7c32db895de3b01bee0132c9209e1", + 96 + ), + withdrawalCredentials: dataToBytes("0x001db70c485b6264692f26b8aeaab5b0c384180df8e2184a21a808a3ec8e86ca", 32), + }; + + sendRawTransactionBig(ethRpcUrl, depositTransactionA, `${dataPath}/deposit.json`).catch((e: Error) => { + loggerExecutionEngine.error("Fail to send raw deposit transaction A", undefined, e); + }); + + sendRawTransactionBig(ethRpcUrl, depositTransactionB, `${dataPath}/deposit.json`).catch((e: Error) => { + loggerExecutionEngine.error("Fail to send raw deposit transaction B", undefined, e); + }); + + // 3. Import new payload with tx A and deposit receipt A + const newPayloadBlockHash = "0xfd1189e6ea0814b7d40d4e50b31ae5feabbb2acff39399457bbdda7cb5ccd490"; + const newPayload = { + parentHash: dataToBytes("0x26118cf71453320edcebbc4ebb34af5b578087a32385b80108bf691fa23efc42", 32), + feeRecipient: dataToBytes("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", 20), + stateRoot: dataToBytes("0x14208ac0e218167936e220b72d5d5887a963cb858ea2f2d268518f014a3da3fa", 32), + logsBloom: dataToBytes( + "0x10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000", + 256 + ), + prevRandao: dataToBytes("0x0000000000000000000000000000000000000000000000000000000000000000", 32), + gasLimit: 30000000, + gasUsed: 84846, + timestamp: 16, + extraData: dataToBytes("0x", 0), + baseFeePerGas: 7n, + excessBlobGas: 0n, + transactions: [dataToBytes(depositTransactionA, null)], + withdrawals: [], + depositReceipts: [depositReceiptA], + blockNumber: 1, + blockHash: dataToBytes(newPayloadBlockHash, 32), + receiptsRoot: dataToBytes("0x79ee3424eb720a3ad4b1c5a372bb8160580cbe4d893778660f34213c685627a9", 32), + blobGasUsed: 0n, + }; + const parentBeaconBlockRoot = dataToBytes("0x0000000000000000000000000000000000000000000000000000000000000000", 32); + const payloadResult = await executionEngine.notifyNewPayload( + ForkName.electra, + newPayload, + [], + parentBeaconBlockRoot + ); + if (payloadResult.status !== ExecutionPayloadStatus.VALID) { + throw Error("getPayload returned payload that notifyNewPayload deems invalid"); + } + + // 4. Update fork choice + const preparePayloadParams2: PayloadAttributes = { + timestamp: 48, + prevRandao: dataToBytes("0x0000000000000000000000000000000000000000000000000000000000000000", 32), + suggestedFeeRecipient: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + withdrawals: [], + parentBeaconBlockRoot: dataToBytes("0x0000000000000000000000000000000000000000000000000000000000000000", 32), + }; + + const payloadId2 = await executionEngine.notifyForkchoiceUpdate( + ForkName.electra, + newPayloadBlockHash, + //use finalizedBlockHash as safeBlockHash + newPayloadBlockHash, + newPayloadBlockHash, + preparePayloadParams2 + ); + if (!payloadId2) throw Error("InvalidPayloadId"); + + // 5. Get the payload. Check depositReceipts field contains deposit + // Wait a bit first for besu to pick up tx from the tx pool. + await sleep(1000); + const payloadAndBlockValue = await executionEngine.getPayload(ForkName.electra, payloadId2); + const payload = payloadAndBlockValue.executionPayload as electra.ExecutionPayload; + + if (payload.transactions.length !== 1) { + throw Error(`Number of transactions mismatched. Expected: 1, actual: ${payload.transactions.length}`); + } else { + const actualTransaction = bytesToData(payload.transactions[0]); + + if (actualTransaction !== depositTransactionB) { + throw Error(`Transaction mismatched. Expected: ${depositTransactionB}, actual: ${actualTransaction}`); + } + } + + if (payload.depositReceipts.length !== 1) { + throw Error(`Number of depositReceipts mismatched. Expected: 1, actual: ${payload.depositReceipts.length}`); + } + + const actualDepositReceipt = payload.depositReceipts[0]; + if (!_.isEqual(actualDepositReceipt, depositReceiptB)) { + throw Error( + `Deposit receipts mismatched. Expected: ${JSON.stringify(depositReceiptB)}, actual: ${JSON.stringify( + actualDepositReceipt + )}` + ); + } + }); + + it("Post-merge, run for a few blocks", async function () { + console.log("\n\nPost-merge, run for a few blocks\n\n"); + const {elClient, tearDownCallBack} = await runEL( + {...elSetupConfig, mode: ELStartMode.PostMerge, genesisTemplate: "electra.tmpl"}, + {...elRunOptions, ttd: BigInt(0)}, + controller.signal + ); + afterEachCallbacks.push(() => tearDownCallBack()); + + await runNodeWithEL({ + elClient, + electraEpoch: 0, + testName: "post-merge", + }); + }); + + /** + * Want to test two things: + * 1) Send two raw deposit transactions, and see if two new validators with corrent balances show up in the state.validators and unfinalized cache + * 2) Upon state-transition, see if the two new validators move from unfinalized cache to finalized cache + */ + async function runNodeWithEL({ + elClient, + electraEpoch, + testName, + }: { + elClient: ELClient; + electraEpoch: Epoch; + testName: string; + }): Promise { + const {genesisBlockHash, ttd, engineRpcUrl, ethRpcUrl} = elClient; + const validatorClientCount = 1; + const validatorsPerClient = 32; + + const testParams: Pick = { + SECONDS_PER_SLOT: 2, + }; + + // Just enough to have a checkpoint finalized + const expectedEpochsToFinish = 4; + // 1 epoch of margin of error + const epochsOfMargin = 1; + const timeoutSetupMargin = 30 * 1000; // Give extra 30 seconds of margin + + // delay a bit so regular sync sees it's up to date and sync is completed from the beginning + const genesisSlotsDelay = 8; + + const timeout = + ((epochsOfMargin + expectedEpochsToFinish) * SLOTS_PER_EPOCH + genesisSlotsDelay) * + testParams.SECONDS_PER_SLOT * + 1000; + + vi.setConfig({testTimeout: timeout + 2 * timeoutSetupMargin}); + + const genesisTime = Math.floor(Date.now() / 1000) + genesisSlotsDelay * testParams.SECONDS_PER_SLOT; + + const testLoggerOpts: TestLoggerOpts = { + level: LogLevel.info, + file: { + filepath: `${logFilesDir}/mergemock-${testName}.log`, + level: LogLevel.debug, + }, + timestampFormat: { + format: TimestampFormatCode.EpochSlot, + genesisTime, + slotsPerEpoch: SLOTS_PER_EPOCH, + secondsPerSlot: testParams.SECONDS_PER_SLOT, + }, + }; + const loggerNodeA = testLogger("Node-A", testLoggerOpts); + + const bn = await getDevBeaconNode({ + params: { + ...testParams, + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: electraEpoch, + TERMINAL_TOTAL_DIFFICULTY: ttd, + }, + options: { + api: {rest: {enabled: true} as BeaconRestApiServerOpts}, + sync: {isSingleNode: true}, + network: {allowPublishToZeroPeers: true, discv5: null}, + // Now eth deposit/merge tracker methods directly available on engine endpoints + eth1: {enabled: false, providerUrls: [engineRpcUrl], jwtSecretHex}, + executionEngine: {urls: [engineRpcUrl], jwtSecretHex}, + chain: {suggestedFeeRecipient: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, + }, + validatorCount: validatorClientCount * validatorsPerClient, + logger: loggerNodeA, + genesisTime, + eth1BlockHash: dataToBytes(genesisBlockHash, 32), + withEth1Credentials: true, + }); + + afterEachCallbacks.push(async function () { + await bn.close(); + await sleep(1000); + }); + + const stopInfoTracker = simTestInfoTracker(bn, loggerNodeA); + const valProposerConfig = { + defaultConfig: { + feeRecipient: "0xcccccccccccccccccccccccccccccccccccccccc", + }, + } as ValidatorProposerConfig; + + const {validators} = await getAndInitDevValidators({ + node: bn, + logPrefix: "Node-A", + validatorsPerClient, + validatorClientCount, + startIndex: 0, + // At least one sim test must use the REST API for beacon <-> validator comms + useRestApi: true, + testLoggerOpts, + valProposerConfig, + }); + + afterEachCallbacks.push(async function () { + await Promise.all(validators.map((v) => v.close())); + }); + + await waitForSlot(bn, 1); + + // send raw tx at slot 1 + const depositTransaction = + "0x02f9021e8217de8085012a05f20085019254d380830271009442424242424242424242424242424242424242428901bc16d674ec800000b901a422895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120ef950826b191ebea0bbafa92a2c6bffa8239c6f456d92891ce2852b8360f0d30000000000000000000000000000000000000000000000000000000000000003095e4f91aea91a9e00387fad9d60997cff6cbf68d42d1b6629a7b248cdef255f94a2a2381e5d4125273fe42da5f7aa0e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020003f5102dabe0a27b1746098d1dc17a5d3fbd478759fea9287e4e419b3c3cef20000000000000000000000000000000000000000000000000000000000000060b6c06e65228046268aa918baf78e072c25e65aa0bcf258cefcac3371c47df81bc4d43ca942f5fc28f9a563e925fd9c5010bc8c300add3faf3af0d61fabaaf03694020feaafb03e47c1bc4fcf082684c7ed3f7d5839d1722214b24f95ad2b226cc080a0be1161617492e4ca2fcb89edcadf5e71e8cac0d6447d18cfde9b55e5a8412417a07ec8c47dd484036c745049bb2e2980d44e38d4dacac50dc4a14a2f23c52f2e5f"; + sendRawTransactionBig(ethRpcUrl, depositTransaction, `${dataPath}/deposit.json`).catch((e: Error) => { + loggerNodeA.error("Fail to send raw deposit transaction", undefined, e); + }); + + await waitForSlot(bn, 5); + // Expect new validator to be in unfinalized cache, in state.validators and not in finalized cache + let headState = bn.chain.getHeadState(); + let epochCtx = headState.epochCtx; + if (headState.validators.length !== 33 || headState.balances.length !== 33) { + throw Error("New validator is not reflected in the beacon state at slot 5"); + } + if (epochCtx.index2pubkey.length !== 32 || epochCtx.pubkey2index.size !== 32) { + throw Error("Finalized cache is modified."); + } + if (epochCtx.unfinalizedPubkey2index.size !== 1) { + throw Error( + `Unfinalized cache is missing the expected validator. Size: ${epochCtx.unfinalizedPubkey2index.size}` + ); + } + // validator count at epoch 1 should be empty at this point since no epoch transition has happened. + if (epochCtx.getValidatorCountAtEpoch(1) !== undefined) { + throw Error("Historical validator lengths is modified"); + } + + await new Promise((resolve, _reject) => { + bn.chain.clock.on(ClockEvent.epoch, (epoch) => { + // Resolve only if the finalized checkpoint includes execution payload + if (epoch >= expectedEpochsToFinish) { + console.log("\nGot event epoch, stopping validators and nodes\n"); + resolve(); + } + }); + }); + + // Stop chain and un-subscribe events so the execution engine won't update it's head + // Allow some time to broadcast finalized events and complete the importBlock routine + await Promise.all(validators.map((v) => v.close())); + await bn.close(); + await sleep(500); + + // Check if new validator is in finalized cache + headState = bn.chain.getHeadState() as CachedBeaconStateElectra; + epochCtx = headState.epochCtx; + + if (headState.validators.length !== 33 || headState.balances.length !== 33) { + throw Error("New validator is not reflected in the beacon state."); + } + if (epochCtx.index2pubkey.length !== 33 || epochCtx.pubkey2index.size !== 33) { + throw Error("New validator is not in finalized cache"); + } + if (!epochCtx.unfinalizedPubkey2index.isEmpty()) { + throw Error("Unfinalized cache still contains new validator"); + } + // After 4 epochs, headState's finalized cp epoch should be 2 + // epochCtx should only have validator count for epoch 3 and 4. + if (epochCtx.getValidatorCountAtEpoch(4) === undefined || epochCtx.getValidatorCountAtEpoch(3) === undefined) { + throw Error("Missing historical validator length for epoch 3 or 4"); + } + + if (epochCtx.getValidatorCountAtEpoch(4) !== 33 || epochCtx.getValidatorCountAtEpoch(3) !== 33) { + throw Error("Incorrect historical validator length for epoch 3 or 4"); + } + + if (epochCtx.getValidatorCountAtEpoch(2) !== undefined || epochCtx.getValidatorCountAtEpoch(1) !== undefined) { + throw Error("Historical validator length for epoch 1 or 2 is not dropped properly"); + } + + if (headState.depositReceiptsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { + throw Error("state.depositReceiptsStartIndex is not set upon processing new deposit receipt"); + } + + // wait for 1 slot to print current epoch stats + await sleep(1 * bn.config.SECONDS_PER_SLOT * 1000); + stopInfoTracker(); + console.log("\n\nDone\n\n"); + } +}); + +async function waitForSlot(bn: BeaconNode, targetSlot: Slot): Promise { + await new Promise((resolve, reject) => { + bn.chain.clock.on(ClockEvent.slot, (currentSlot) => { + if (currentSlot === targetSlot) { + resolve(); + return; + } + if (currentSlot > targetSlot) { + reject(Error(`Beacon node has passed target slot ${targetSlot}. Current slot ${currentSlot}`)); + } + }); + }); +} diff --git a/packages/beacon-node/test/spec/presets/fork.test.ts b/packages/beacon-node/test/spec/presets/fork.test.ts index c880d24bbbe3..c121e651fcea 100644 --- a/packages/beacon-node/test/spec/presets/fork.test.ts +++ b/packages/beacon-node/test/spec/presets/fork.test.ts @@ -5,6 +5,7 @@ import { CachedBeaconStateAltair, CachedBeaconStatePhase0, CachedBeaconStateCapella, + CachedBeaconStateDeneb, } from "@lodestar/state-transition"; import * as slotFns from "@lodestar/state-transition/slot"; import {phase0, ssz} from "@lodestar/types"; @@ -36,7 +37,7 @@ const fork: TestRunnerFn = (forkNext) => { case ForkName.deneb: return slotFns.upgradeStateToDeneb(preState as CachedBeaconStateCapella); case ForkName.electra: - throw Error("not Implemented"); + return slotFns.upgradeStateToElectra(preState as CachedBeaconStateDeneb); } }, options: { diff --git a/packages/beacon-node/test/spec/presets/genesis.test.ts b/packages/beacon-node/test/spec/presets/genesis.test.ts index ba3351a2103c..aa052c52a270 100644 --- a/packages/beacon-node/test/spec/presets/genesis.test.ts +++ b/packages/beacon-node/test/spec/presets/genesis.test.ts @@ -1,6 +1,6 @@ import path from "node:path"; import {expect} from "vitest"; -import {phase0, Root, ssz, TimeSeconds, allForks, deneb} from "@lodestar/types"; +import {phase0, Root, ssz, TimeSeconds, allForks, electra} from "@lodestar/types"; import {InputType} from "@lodestar/spec-test-util"; import { BeaconStateAllForks, @@ -60,7 +60,7 @@ const genesisInitialization: TestRunnerFn> = blockFns.processDeposit(fork, state, testCase.deposit); }, + deposit_receipt: (state, testCase: {deposit_receipt: electra.DepositReceipt}) => { + const fork = state.config.getForkSeq(state.slot); + blockFns.processDepositReceipt(fork, state as CachedBeaconStateElectra, testCase.deposit_receipt); + }, + proposer_slashing: (state, testCase: {proposer_slashing: phase0.ProposerSlashing}) => { const fork = state.config.getForkSeq(state.slot); blockFns.processProposerSlashing(fork, state, testCase.proposer_slashing); @@ -121,6 +127,7 @@ const operations: TestRunnerFn = (fork, block: ssz[fork].BeaconBlock, body: ssz[fork].BeaconBlockBody, deposit: ssz.phase0.Deposit, + deposit_receipt: ssz.electra.DepositReceipt, proposer_slashing: ssz.phase0.ProposerSlashing, voluntary_exit: ssz.phase0.SignedVoluntaryExit, // Altair diff --git a/packages/beacon-node/test/spec/presets/ssz_static.test.ts b/packages/beacon-node/test/spec/presets/ssz_static.test.ts index bcab25acde4c..c77a3377c433 100644 --- a/packages/beacon-node/test/spec/presets/ssz_static.test.ts +++ b/packages/beacon-node/test/spec/presets/ssz_static.test.ts @@ -48,6 +48,7 @@ const sszStatic = // will help us get the right type for lightclient objects ((ssz.allForksLightClient[fork as ForkLightClient] || {}) as Types)[typeName] || (ssz[fork] as Types)[typeName] || + (ssz.deneb as Types)[typeName] || (ssz.capella as Types)[typeName] || (ssz.bellatrix as Types)[typeName] || (ssz.altair as Types)[typeName] || diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 6aa683cb6530..3201fb15dbf3 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -57,7 +57,6 @@ const coveredTestRunners = [ // ], // ``` export const defaultSkipOpts: SkipOpts = { - skippedForks: ["eip6110"], // TODO: capella // BeaconBlockBody proof in lightclient is the new addition in v1.3.0-rc.2-hotfix // Skip them for now to enable subsequently diff --git a/packages/beacon-node/test/unit/chain/lightclient/upgradeLightClientHeader.test.ts b/packages/beacon-node/test/unit/chain/lightclient/upgradeLightClientHeader.test.ts index 9df60a39af4d..8a33beffe21e 100644 --- a/packages/beacon-node/test/unit/chain/lightclient/upgradeLightClientHeader.test.ts +++ b/packages/beacon-node/test/unit/chain/lightclient/upgradeLightClientHeader.test.ts @@ -15,7 +15,7 @@ describe("UpgradeLightClientHeader", function () { BELLATRIX_FORK_EPOCH: 2, CAPELLA_FORK_EPOCH: 3, DENEB_FORK_EPOCH: 4, - ELECTRA_FORK_EPOCH: Infinity, + ELECTRA_FORK_EPOCH: 5, }); const genesisValidatorsRoot = Buffer.alloc(32, 0xaa); @@ -37,18 +37,12 @@ describe("UpgradeLightClientHeader", function () { bellatrix: 17, capella: 25, deneb: 33, - electra: 0, + electra: 41, }; }); - // Since electra is not implemented for loop is till deneb (Object.values(ForkName).length-1) - // Once electra is implemnted run for loop till Object.values(ForkName).length - - // for (let i = ForkSeq.altair; i < Object.values(ForkName).length; i++) { - // for (let j = i + 1; j < Object.values(ForkName).length; j++) { - - for (let i = ForkSeq.altair; i < Object.values(ForkName).length - 1; i++) { - for (let j = i + 1; j < Object.values(ForkName).length - 1; j++) { + for (let i = ForkSeq.altair; i < Object.values(ForkName).length; i++) { + for (let j = i + 1; j < Object.values(ForkName).length; j++) { const fromFork = ForkName[ForkSeq[i] as ForkName]; const toFork = ForkName[ForkSeq[j] as ForkName]; @@ -62,27 +56,7 @@ describe("UpgradeLightClientHeader", function () { } } - // for electra not implemented for (let i = ForkSeq.altair; i < Object.values(ForkName).length; i++) { - const fromFork = ForkName[ForkSeq[i] as ForkName]; - const toFork = ForkName["electra"]; - - it(`Throw error ${fromFork}=>${toFork}`, function () { - lcHeaderByFork[fromFork].beacon.slot = testSlots[fromFork]; - lcHeaderByFork[toFork].beacon.slot = testSlots[fromFork]; - - expect(() => { - upgradeLightClientHeader(config, toFork, lcHeaderByFork[fromFork]); - }).toThrow("Not Implemented"); - }); - } - - // Since electra is not implemented for loop is till deneb (Object.values(ForkName).length-1) - // Once electra is implemnted run for loop till Object.values(ForkName).length - - // for (let i = ForkSeq.altair; i < Object.values(ForkName).length; i++) { - - for (let i = ForkSeq.altair; i < Object.values(ForkName).length - 1; i++) { for (let j = i; j > 0; j--) { const fromFork = ForkName[ForkSeq[i] as ForkName]; const toFork = ForkName[ForkSeq[j] as ForkName]; 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 3c248ad4d194..71dfd63830c1 100644 --- a/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts @@ -62,9 +62,9 @@ describe("AggregatedAttestationPool", function () { epochParticipation[committee[i]] = 0b000; } } - (originalState as CachedBeaconStateAltair).previousEpochParticipation = + (originalState as unknown as CachedBeaconStateAltair).previousEpochParticipation = ssz.altair.EpochParticipation.toViewDU(epochParticipation); - (originalState as CachedBeaconStateAltair).currentEpochParticipation = + (originalState as unknown 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 6295a993c072..e6a7e8706bbe 100644 --- a/packages/beacon-node/test/unit/chain/shufflingCache.test.ts +++ b/packages/beacon-node/test/unit/chain/shufflingCache.test.ts @@ -1,6 +1,6 @@ import {describe, it, expect, beforeEach} from "vitest"; -import {getShufflingDecisionBlock} from "@lodestar/state-transition"; +import {getShufflingDecisionBlock, CachedBeaconStateAllForks} from "@lodestar/state-transition"; // eslint-disable-next-line import/no-relative-packages import {generateTestCachedBeaconStateOnlyValidators} from "../../../../state-transition/test/perf/util.js"; import {ShufflingCache} from "../../../src/chain/shufflingCache.js"; @@ -14,7 +14,7 @@ describe("ShufflingCache", function () { beforeEach(() => { shufflingCache = new ShufflingCache(null, {maxShufflingCacheEpochs: 1}); - shufflingCache.processState(state, currentEpoch); + shufflingCache.processState(state as unknown 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, currentEpoch + 1); + shufflingCache.processState(state as unknown 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, currentEpoch + 1); + shufflingCache.processState(state as unknown 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/chain/stateCache/fifoBlockStateCache.test.ts b/packages/beacon-node/test/unit/chain/stateCache/fifoBlockStateCache.test.ts index 7d3f34ddac36..b4aac92dd9bb 100644 --- a/packages/beacon-node/test/unit/chain/stateCache/fifoBlockStateCache.test.ts +++ b/packages/beacon-node/test/unit/chain/stateCache/fifoBlockStateCache.test.ts @@ -1,8 +1,7 @@ import {describe, it, expect, beforeEach} from "vitest"; import {toHexString} from "@chainsafe/ssz"; -import {EpochShuffling} from "@lodestar/state-transition"; +import {EpochShuffling, CachedBeaconStateAllForks} from "@lodestar/state-transition"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; -import {CachedBeaconStateAllForks} from "@lodestar/state-transition/src/types.js"; import {FIFOBlockStateCache} from "../../../../src/chain/stateCache/index.js"; import {generateCachedState} from "../../../utils/state.js"; diff --git a/packages/beacon-node/test/unit/eth1/utils/deposits.test.ts b/packages/beacon-node/test/unit/eth1/utils/deposits.test.ts index ce0d7fae1fad..6cc3ae8f1ce0 100644 --- a/packages/beacon-node/test/unit/eth1/utils/deposits.test.ts +++ b/packages/beacon-node/test/unit/eth1/utils/deposits.test.ts @@ -1,13 +1,15 @@ import {describe, it, expect} from "vitest"; import {phase0, ssz} from "@lodestar/types"; -import {MAX_DEPOSITS} from "@lodestar/params"; +import {MAX_DEPOSITS, SLOTS_PER_EPOCH} from "@lodestar/params"; import {verifyMerkleBranch} from "@lodestar/utils"; +import {createChainForkConfig} from "@lodestar/config"; import {filterBy} from "../../../utils/db.js"; import {Eth1ErrorCode} from "../../../../src/eth1/errors.js"; import {generateState} from "../../../utils/state.js"; import {expectRejectedWithLodestarError} from "../../../utils/errors.js"; import {getDeposits, getDepositsWithProofs, DepositGetter} from "../../../../src/eth1/utils/deposits.js"; import {DepositTree} from "../../../../src/db/repositories/depositDataRoot.js"; +import {createCachedBeaconStateTest} from "../../../utils/cachedBeaconState.js"; describe("eth1 / util / deposits", function () { describe("getDeposits", () => { @@ -18,6 +20,7 @@ describe("eth1 / util / deposits", function () { depositIndexes: number[]; expectedReturnedIndexes?: number[]; error?: Eth1ErrorCode; + postElectra?: boolean; }; const testCases: TestCase[] = [ @@ -70,18 +73,59 @@ describe("eth1 / util / deposits", function () { depositIndexes: [], expectedReturnedIndexes: [], }, + { + id: "No deposits to be included post Electra after deposit_receipts_start_index", + depositCount: 2030, + eth1DepositIndex: 2025, + depositIndexes: Array.from({length: 2030}, (_, i) => i), + expectedReturnedIndexes: [], + postElectra: true, + }, + { + id: "Should return deposits post Electra before deposit_receipts_start_index", + depositCount: 2022, + eth1DepositIndex: 2018, + depositIndexes: Array.from({length: 2022}, (_, i) => i), + expectedReturnedIndexes: [2018, 2019, 2020, 2021], + postElectra: true, + }, + { + id: "Should return deposits less than MAX_DEPOSITS post Electra before deposit_receipts_start_index", + depositCount: 10 * MAX_DEPOSITS, + eth1DepositIndex: 0, + depositIndexes: Array.from({length: 10 * MAX_DEPOSITS}, (_, i) => i), + expectedReturnedIndexes: Array.from({length: MAX_DEPOSITS}, (_, i) => i), + postElectra: true, + }, ]; + /* eslint-disable @typescript-eslint/naming-convention */ + const postElectraConfig = createChainForkConfig({ + ALTAIR_FORK_EPOCH: 1, + BELLATRIX_FORK_EPOCH: 2, + CAPELLA_FORK_EPOCH: 3, + DENEB_FORK_EPOCH: 4, + ELECTRA_FORK_EPOCH: 5, + }); + const postElectraSlot = postElectraConfig.ELECTRA_FORK_EPOCH * SLOTS_PER_EPOCH + 1; + for (const testCase of testCases) { - const {id, depositIndexes, eth1DepositIndex, depositCount, expectedReturnedIndexes, error} = testCase; + const {id, depositIndexes, eth1DepositIndex, depositCount, expectedReturnedIndexes, error, postElectra} = + testCase; it(id, async function () { - const state = generateState({eth1DepositIndex}); + const state = postElectra + ? generateState({slot: postElectraSlot, eth1DepositIndex}, postElectraConfig) + : generateState({eth1DepositIndex}); + const cachedState = createCachedBeaconStateTest( + state, + postElectra ? postElectraConfig : createChainForkConfig({}) + ); const eth1Data = generateEth1Data(depositCount); const deposits = depositIndexes.map((index) => generateDepositEvent(index)); const depositsGetter: DepositGetter = async (indexRange) => filterBy(deposits, indexRange, (deposit) => deposit.index); - const resultPromise = getDeposits(state, eth1Data, depositsGetter); + const resultPromise = getDeposits(cachedState, eth1Data, depositsGetter); if (expectedReturnedIndexes) { const result = await resultPromise; diff --git a/packages/beacon-node/test/unit/executionEngine/http.test.ts b/packages/beacon-node/test/unit/executionEngine/http.test.ts index 27e9b86887ee..d4a7f3a476e9 100644 --- a/packages/beacon-node/test/unit/executionEngine/http.test.ts +++ b/packages/beacon-node/test/unit/executionEngine/http.test.ts @@ -188,6 +188,7 @@ describe("ExecutionEngine / http", () => { amount: "0x7b", }, ], + depositReceipts: null, // depositReceipts is null pre-electra }, null, // null returned for missing blocks { @@ -196,6 +197,7 @@ describe("ExecutionEngine / http", () => { "0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174", ], withdrawals: null, // withdrawals is null pre-capella + depositReceipts: null, // depositReceipts is null pre-electra }, ], }; @@ -243,6 +245,7 @@ describe("ExecutionEngine / http", () => { amount: "0x7b", }, ], + depositReceipts: null, // depositReceipts is null pre-electra }, null, // null returned for missing blocks { @@ -251,6 +254,7 @@ describe("ExecutionEngine / http", () => { "0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174", ], withdrawals: null, // withdrawals is null pre-capella + depositReceipts: null, // depositReceipts is null pre-electra }, ], }; diff --git a/packages/beacon-node/test/unit/network/beaconBlocksMaybeBlobsByRange.test.ts b/packages/beacon-node/test/unit/network/beaconBlocksMaybeBlobsByRange.test.ts index 3fb9cb8e1c79..c7de35d468df 100644 --- a/packages/beacon-node/test/unit/network/beaconBlocksMaybeBlobsByRange.test.ts +++ b/packages/beacon-node/test/unit/network/beaconBlocksMaybeBlobsByRange.test.ts @@ -24,6 +24,7 @@ describe("beaconBlocksMaybeBlobsByRange", () => { BELLATRIX_FORK_EPOCH: 0, CAPELLA_FORK_EPOCH: 0, DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: 0, }); const genesisValidatorsRoot = Buffer.alloc(32, 0xaa); const config = createBeaconConfig(chainConfig, genesisValidatorsRoot); diff --git a/packages/beacon-node/test/utils/state.ts b/packages/beacon-node/test/utils/state.ts index af9ebd479340..04e1dc710b5e 100644 --- a/packages/beacon-node/test/utils/state.ts +++ b/packages/beacon-node/test/utils/state.ts @@ -7,8 +7,10 @@ import { PubkeyIndexMap, CachedBeaconStateBellatrix, BeaconStateBellatrix, + CachedBeaconStateElectra, + BeaconStateElectra, } from "@lodestar/state-transition"; -import {allForks, altair, bellatrix, ssz} from "@lodestar/types"; +import {allForks, altair, bellatrix, electra, ssz} from "@lodestar/types"; import {createBeaconConfig, ChainForkConfig} from "@lodestar/config"; import {FAR_FUTURE_EPOCH, ForkName, ForkSeq, MAX_EFFECTIVE_BALANCE, SYNC_COMMITTEE_SIZE} from "@lodestar/params"; @@ -64,6 +66,7 @@ export function generateState( : generateValidators(numValidators, validatorOpts)); state.genesisTime = Math.floor(Date.now() / 1000); + state.slot = stateSlot; state.fork.previousVersion = config.GENESIS_FORK_VERSION; state.fork.currentVersion = config.GENESIS_FORK_VERSION; state.latestBlockHeader.bodyRoot = ssz.phase0.BeaconBlockBody.hashTreeRoot(ssz.phase0.BeaconBlockBody.defaultValue()); @@ -92,6 +95,12 @@ export function generateState( }; } + if (forkSeq >= ForkSeq.electra) { + const stateElectra = state as electra.BeaconState; + stateElectra.depositReceiptsStartIndex = 2023n; + stateElectra.latestExecutionPayloadHeader = ssz.electra.ExecutionPayloadHeader.defaultValue(); + } + return config.getForkTypes(stateSlot).BeaconState.toViewDU(state); } @@ -137,6 +146,18 @@ export function generateCachedBellatrixState(opts?: TestBeaconState): CachedBeac }); } +/** + * This generates state with default pubkey + */ +export function generateCachedElectraState(opts?: TestBeaconState): CachedBeaconStateElectra { + const config = getConfig(ForkName.electra); + const state = generateState(opts, config); + return createCachedBeaconState(state as BeaconStateElectra, { + config: createBeaconConfig(config, state.genesisValidatorsRoot), + pubkey2index: new PubkeyIndexMap(), + index2pubkey: [], + }); +} export const zeroProtoBlock: ProtoBlock = { slot: 0, blockRoot: ZERO_HASH_HEX, diff --git a/packages/beacon-node/test/utils/validationData/attestation.ts b/packages/beacon-node/test/utils/validationData/attestation.ts index c33d942dabc5..335ec81870c6 100644 --- a/packages/beacon-node/test/utils/validationData/attestation.ts +++ b/packages/beacon-node/test/utils/validationData/attestation.ts @@ -4,6 +4,7 @@ import { computeSigningRoot, computeStartSlotAtEpoch, getShufflingDecisionBlock, + CachedBeaconStateAllForks, } from "@lodestar/state-transition"; import {ProtoBlock, IForkChoice, ExecutionStatus, DataAvailabilityStatus} from "@lodestar/fork-choice"; import {DOMAIN_BEACON_ATTESTER} from "@lodestar/params"; @@ -82,8 +83,8 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { }; const shufflingCache = new ShufflingCache(); - shufflingCache.processState(state, state.epochCtx.currentShuffling.epoch); - shufflingCache.processState(state, state.epochCtx.nextShuffling.epoch); + shufflingCache.processState(state as unknown as CachedBeaconStateAllForks, state.epochCtx.currentShuffling.epoch); + shufflingCache.processState(state as unknown as CachedBeaconStateAllForks, state.epochCtx.nextShuffling.epoch); const dependentRoot = getShufflingDecisionBlock(state, state.epochCtx.currentShuffling.epoch); const forkChoice = { @@ -133,7 +134,7 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { getState: async () => state, // TODO: remove this once we have a better way to get state getStateSync: () => state, - } as Partial as IStateRegenerator; + } as unknown as Partial as IStateRegenerator; const chain = { clock, diff --git a/packages/config/src/chainConfig/configs/mainnet.ts b/packages/config/src/chainConfig/configs/mainnet.ts index 0de1bee666ec..6d1fd9b75952 100644 --- a/packages/config/src/chainConfig/configs/mainnet.ts +++ b/packages/config/src/chainConfig/configs/mainnet.ts @@ -49,7 +49,7 @@ export const chainConfig: ChainConfig = { DENEB_FORK_VERSION: b("0x04000000"), DENEB_FORK_EPOCH: 269568, // March 13, 2024, 01:55:35pm UTC - // Electra + // ELECTRA ELECTRA_FORK_VERSION: b("0x05000000"), ELECTRA_FORK_EPOCH: Infinity, diff --git a/packages/config/src/chainConfig/configs/minimal.ts b/packages/config/src/chainConfig/configs/minimal.ts index c99a76d1ee40..44a28ca36ec7 100644 --- a/packages/config/src/chainConfig/configs/minimal.ts +++ b/packages/config/src/chainConfig/configs/minimal.ts @@ -45,8 +45,7 @@ export const chainConfig: ChainConfig = { // Deneb DENEB_FORK_VERSION: b("0x04000001"), DENEB_FORK_EPOCH: Infinity, - - // Electra + // ELECTRA ELECTRA_FORK_VERSION: b("0x05000001"), ELECTRA_FORK_EPOCH: Infinity, diff --git a/packages/light-client/src/spec/utils.ts b/packages/light-client/src/spec/utils.ts index ed4b9e6e961d..8502cb6f5656 100644 --- a/packages/light-client/src/spec/utils.ts +++ b/packages/light-client/src/spec/utils.ts @@ -8,7 +8,7 @@ import { BLOCK_BODY_EXECUTION_PAYLOAD_DEPTH as EXECUTION_PAYLOAD_DEPTH, BLOCK_BODY_EXECUTION_PAYLOAD_INDEX as EXECUTION_PAYLOAD_INDEX, } from "@lodestar/params"; -import {altair, phase0, ssz, allForks, capella, deneb, Slot} from "@lodestar/types"; +import {altair, phase0, ssz, allForks, capella, deneb, Slot, electra} from "@lodestar/types"; import {ChainForkConfig} from "@lodestar/config"; import {isValidMerkleBranch, computeEpochAtSlot, computeSyncPeriodAtSlot} from "../utils/index.js"; @@ -106,7 +106,11 @@ export function upgradeLightClientHeader( // eslint-disable-next-line no-fallthrough case ForkName.electra: - throw Error("Not Implemented"); + (upgradedHeader as electra.LightClientHeader).execution.depositReceiptsRoot = + ssz.electra.LightClientHeader.fields.execution.fields.depositReceiptsRoot.defaultValue(); + + // Break if no further upgrades is required else fall through + if (ForkSeq[targetFork] <= ForkSeq.electra) break; } return upgradedHeader; } @@ -140,6 +144,12 @@ export function isValidLightClientHeader(config: ChainForkConfig, header: allFor } } + if (epoch < config.ELECTRA_FORK_EPOCH) { + if ((header as electra.LightClientHeader).execution.depositReceiptsRoot !== undefined) { + return false; + } + } + return isValidMerkleBranch( config .getExecutionForkTypes(header.beacon.slot) diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index 6a95e3ca632e..482e591123b6 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -93,6 +93,8 @@ export const { MAX_BLOB_COMMITMENTS_PER_BLOCK, MAX_BLOBS_PER_BLOCK, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH, + + MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD, } = activePreset; //////////// @@ -244,3 +246,6 @@ export const KZG_COMMITMENT_SUBTREE_INDEX0 = KZG_COMMITMENT_GINDEX0 - 2 ** KZG_C // ssz.deneb.BlobSidecars.elementType.fixedSize export const BLOBSIDECAR_FIXED_SIZE = ACTIVE_PRESET === PresetName.minimal ? 131672 : 131928; + +// Electra Misc +export const UNSET_DEPOSIT_RECEIPTS_START_INDEX = 2n ** 64n - 1n; diff --git a/packages/params/src/presets/mainnet.ts b/packages/params/src/presets/mainnet.ts index 42a705a07f03..86f5c39c539e 100644 --- a/packages/params/src/presets/mainnet.ts +++ b/packages/params/src/presets/mainnet.ts @@ -118,4 +118,7 @@ export const mainnetPreset: BeaconPreset = { MAX_BLOB_COMMITMENTS_PER_BLOCK: 4096, MAX_BLOBS_PER_BLOCK: 6, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 17, + + // ELECTRA + MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 8192, }; diff --git a/packages/params/src/presets/minimal.ts b/packages/params/src/presets/minimal.ts index b940841a0429..6239a8c1a3eb 100644 --- a/packages/params/src/presets/minimal.ts +++ b/packages/params/src/presets/minimal.ts @@ -119,4 +119,7 @@ export const minimalPreset: BeaconPreset = { MAX_BLOB_COMMITMENTS_PER_BLOCK: 16, MAX_BLOBS_PER_BLOCK: 6, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: 9, + + // ELECTRA + MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 4, }; diff --git a/packages/params/src/types.ts b/packages/params/src/types.ts index 3c5ba6381131..57ee8230a77d 100644 --- a/packages/params/src/types.ts +++ b/packages/params/src/types.ts @@ -82,6 +82,9 @@ export type BeaconPreset = { MAX_BLOB_COMMITMENTS_PER_BLOCK: number; MAX_BLOBS_PER_BLOCK: number; KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: number; + + // ELECTRA + MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: number; }; /** @@ -167,6 +170,9 @@ export const beaconPresetTypes: BeaconPresetTypes = { MAX_BLOB_COMMITMENTS_PER_BLOCK: "number", MAX_BLOBS_PER_BLOCK: "number", KZG_COMMITMENT_INCLUSION_PROOF_DEPTH: "number", + + // ELECTRA + MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: "number", }; type BeaconPresetTypes = { diff --git a/packages/state-transition/package.json b/packages/state-transition/package.json index 76b7f52325a3..674058780101 100644 --- a/packages/state-transition/package.json +++ b/packages/state-transition/package.json @@ -68,7 +68,12 @@ "@lodestar/params": "^1.19.0", "@lodestar/types": "^1.19.0", "@lodestar/utils": "^1.19.0", - "bigint-buffer": "^1.1.5" + "bigint-buffer": "^1.1.5", + "buffer-xor": "^2.0.2", + "immutable": "^4.3.2" + }, + "devDependencies": { + "@types/buffer-xor": "^2.0.0" }, "keywords": [ "ethereum", diff --git a/packages/state-transition/src/block/processDeposit.ts b/packages/state-transition/src/block/processDeposit.ts index dae37d5d0afe..fbefafdc7fbf 100644 --- a/packages/state-transition/src/block/processDeposit.ts +++ b/packages/state-transition/src/block/processDeposit.ts @@ -1,6 +1,6 @@ import bls from "@chainsafe/bls"; import {CoordType} from "@chainsafe/bls/types"; -import {phase0, ssz} from "@lodestar/types"; +import {BLSPubkey, Bytes32, UintNum64, phase0, ssz} from "@lodestar/types"; import {verifyMerkleBranch} from "@lodestar/utils"; import { @@ -12,6 +12,8 @@ import { MAX_EFFECTIVE_BALANCE, } from "@lodestar/params"; +import {DepositData} from "@lodestar/types/lib/phase0/types.js"; +import {DepositReceipt} from "@lodestar/types/lib/electra/types.js"; import {ZERO_HASH} from "../constants/index.js"; import {computeDomain, computeSigningRoot, increaseBalance} from "../util/index.js"; import {CachedBeaconStateAllForks, CachedBeaconStateAltair} from "../types.js"; @@ -23,8 +25,6 @@ import {CachedBeaconStateAllForks, CachedBeaconStateAltair} from "../types.js"; * PERF: Work depends on number of Deposit per block. On regular networks the average is 0 / block. */ export function processDeposit(fork: ForkSeq, state: CachedBeaconStateAllForks, deposit: phase0.Deposit): void { - const {config, validators, epochCtx} = state; - // verify the merkle branch if ( !verifyMerkleBranch( @@ -41,15 +41,29 @@ export function processDeposit(fork: ForkSeq, state: CachedBeaconStateAllForks, // deposits must be processed in order state.eth1DepositIndex += 1; - const pubkey = deposit.data.pubkey; // Drop tree - const amount = deposit.data.amount; - const cachedIndex = epochCtx.pubkey2index.get(pubkey); + applyDeposit(fork, state, deposit.data); +} + +/** + * Adds a new validator into the registry. Or increase balance if already exist. + * Follows applyDeposit() in consensus spec. Will be used by processDeposit() and processDepositReceipt() + * + */ +export function applyDeposit( + fork: ForkSeq, + state: CachedBeaconStateAllForks, + deposit: DepositData | DepositReceipt +): void { + const {config, validators, epochCtx} = state; + const {pubkey, withdrawalCredentials, amount} = deposit; + + const cachedIndex = epochCtx.getValidatorIndex(pubkey); if (cachedIndex === undefined || !Number.isSafeInteger(cachedIndex) || cachedIndex >= validators.length) { // verify the deposit signature (proof of posession) which is not checked by the deposit contract const depositMessage = { - pubkey: deposit.data.pubkey, // Retain tree for hashing - withdrawalCredentials: deposit.data.withdrawalCredentials, // Retain tree for hashing - amount: deposit.data.amount, + pubkey, + withdrawalCredentials, + amount, }; // fork-agnostic domain since deposits are valid across forks const domain = computeDomain(DOMAIN_DEPOSIT, config.GENESIS_FORK_VERSION, ZERO_HASH); @@ -57,52 +71,62 @@ export function processDeposit(fork: ForkSeq, state: CachedBeaconStateAllForks, try { // Pubkeys must be checked for group + inf. This must be done only once when the validator deposit is processed const publicKey = bls.PublicKey.fromBytes(pubkey, CoordType.affine, true); - const signature = bls.Signature.fromBytes(deposit.data.signature, CoordType.affine, true); + const signature = bls.Signature.fromBytes(deposit.signature, CoordType.affine, true); if (!signature.verify(publicKey, signingRoot)) { return; } } catch (e) { return; // Catch all BLS errors: failed key validation, failed signature validation, invalid signature } + addValidatorToRegistry(fork, state, pubkey, withdrawalCredentials, amount); + } else { + // increase balance by deposit amount + increaseBalance(state, cachedIndex, amount); + } +} - // add validator and balance entries - const effectiveBalance = Math.min(amount - (amount % EFFECTIVE_BALANCE_INCREMENT), MAX_EFFECTIVE_BALANCE); - validators.push( - ssz.phase0.Validator.toViewDU({ - pubkey, - withdrawalCredentials: deposit.data.withdrawalCredentials, - activationEligibilityEpoch: FAR_FUTURE_EPOCH, - activationEpoch: FAR_FUTURE_EPOCH, - exitEpoch: FAR_FUTURE_EPOCH, - withdrawableEpoch: FAR_FUTURE_EPOCH, - effectiveBalance, - slashed: false, - }) - ); - state.balances.push(amount); +function addValidatorToRegistry( + fork: ForkSeq, + state: CachedBeaconStateAllForks, + pubkey: BLSPubkey, + withdrawalCredentials: Bytes32, + amount: UintNum64 +): void { + const {validators, epochCtx} = state; + // add validator and balance entries + const effectiveBalance = Math.min(amount - (amount % EFFECTIVE_BALANCE_INCREMENT), MAX_EFFECTIVE_BALANCE); + validators.push( + ssz.phase0.Validator.toViewDU({ + pubkey, + withdrawalCredentials, + activationEligibilityEpoch: FAR_FUTURE_EPOCH, + activationEpoch: FAR_FUTURE_EPOCH, + exitEpoch: FAR_FUTURE_EPOCH, + withdrawableEpoch: FAR_FUTURE_EPOCH, + effectiveBalance, + slashed: false, + }) + ); + state.balances.push(amount); - const validatorIndex = validators.length - 1; - // Updating here is better than updating at once on epoch transition - // - Simplify genesis fn applyDeposits(): effectiveBalanceIncrements is populated immediately - // - Keep related code together to reduce risk of breaking this cache - // - Should have equal performance since it sets a value in a flat array - epochCtx.effectiveBalanceIncrementsSet(validatorIndex, effectiveBalance); + const validatorIndex = validators.length - 1; + // Updating here is better than updating at once on epoch transition + // - Simplify genesis fn applyDeposits(): effectiveBalanceIncrements is populated immediately + // - Keep related code together to reduce risk of breaking this cache + // - Should have equal performance since it sets a value in a flat array + epochCtx.effectiveBalanceIncrementsSet(validatorIndex, effectiveBalance); - // now that there is a new validator, update the epoch context with the new pubkey - epochCtx.addPubkey(validatorIndex, pubkey); + // now that there is a new validator, update the epoch context with the new pubkey + epochCtx.addPubkey(validatorIndex, pubkey); - // Only after altair: - if (fork >= ForkSeq.altair) { - const stateAltair = state as CachedBeaconStateAltair; + // Only after altair: + if (fork >= ForkSeq.altair) { + const stateAltair = state as CachedBeaconStateAltair; - stateAltair.inactivityScores.push(0); + stateAltair.inactivityScores.push(0); - // add participation caches - stateAltair.previousEpochParticipation.push(0); - stateAltair.currentEpochParticipation.push(0); - } - } else { - // increase balance by deposit amount - increaseBalance(state, cachedIndex, amount); + // add participation caches + stateAltair.previousEpochParticipation.push(0); + stateAltair.currentEpochParticipation.push(0); } } diff --git a/packages/state-transition/src/block/processDepositReceipt.ts b/packages/state-transition/src/block/processDepositReceipt.ts new file mode 100644 index 000000000000..140e38d634fd --- /dev/null +++ b/packages/state-transition/src/block/processDepositReceipt.ts @@ -0,0 +1,17 @@ +import {electra} from "@lodestar/types"; +import {ForkSeq, UNSET_DEPOSIT_RECEIPTS_START_INDEX} from "@lodestar/params"; + +import {CachedBeaconStateElectra} from "../types.js"; +import {applyDeposit} from "./processDeposit.js"; + +export function processDepositReceipt( + fork: ForkSeq, + state: CachedBeaconStateElectra, + depositReceipt: electra.DepositReceipt +): void { + if (state.depositReceiptsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { + state.depositReceiptsStartIndex = BigInt(depositReceipt.index); + } + + applyDeposit(fork, state, depositReceipt); +} diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index c523f1dad246..968182fdf8c5 100644 --- a/packages/state-transition/src/block/processOperations.ts +++ b/packages/state-transition/src/block/processOperations.ts @@ -1,13 +1,15 @@ -import {allForks, capella} from "@lodestar/types"; -import {ForkSeq, MAX_DEPOSITS} from "@lodestar/params"; +import {allForks, capella, electra} from "@lodestar/types"; +import {ForkSeq} from "@lodestar/params"; -import {CachedBeaconStateAllForks, CachedBeaconStateCapella} from "../types.js"; +import {CachedBeaconStateAllForks, CachedBeaconStateCapella, CachedBeaconStateElectra} from "../types.js"; +import {getEth1DepositCount} from "../util/deposit.js"; import {processAttestations} from "./processAttestations.js"; import {processProposerSlashing} from "./processProposerSlashing.js"; import {processAttesterSlashing} from "./processAttesterSlashing.js"; import {processDeposit} from "./processDeposit.js"; import {processVoluntaryExit} from "./processVoluntaryExit.js"; import {processBlsToExecutionChange} from "./processBlsToExecutionChange.js"; +import {processDepositReceipt} from "./processDepositReceipt.js"; import {ProcessBlockOpts} from "./types.js"; export { @@ -17,6 +19,7 @@ export { processDeposit, processVoluntaryExit, processBlsToExecutionChange, + processDepositReceipt, }; export function processOperations( @@ -26,7 +29,7 @@ export function processOperations( opts: ProcessBlockOpts = {verifySignatures: true} ): void { // verify that outstanding deposits are processed up to the maximum number of deposits - const maxDeposits = Math.min(MAX_DEPOSITS, state.eth1Data.depositCount - state.eth1DepositIndex); + const maxDeposits = getEth1DepositCount(state); if (body.deposits.length !== maxDeposits) { throw new Error( `Block contains incorrect number of deposits: depositCount=${body.deposits.length} expected=${maxDeposits}` @@ -54,4 +57,10 @@ export function processOperations( processBlsToExecutionChange(state as CachedBeaconStateCapella, blsToExecutionChange); } } + + if (fork >= ForkSeq.electra) { + for (const depositReceipt of (body as electra.BeaconBlockBody).executionPayload.depositReceipts) { + processDepositReceipt(fork, state as CachedBeaconStateElectra, depositReceipt); + } + } } diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 9565898eb09d..97834c2afb2c 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -1,5 +1,7 @@ -import {CoordType} from "@chainsafe/bls/types"; +import {CoordType, PublicKey} from "@chainsafe/bls/types"; import bls from "@chainsafe/bls"; +import * as immutable from "immutable"; +import {fromHexString} from "@chainsafe/ssz"; import {BLSSignature, CommitteeIndex, Epoch, Slot, ValidatorIndex, phase0, SyncPeriod} from "@lodestar/types"; import {createBeaconConfig, BeaconConfig, ChainConfig} from "@lodestar/config"; import { @@ -30,8 +32,17 @@ import {computeEpochShuffling, EpochShuffling, getShufflingDecisionBlock} from " import {computeBaseRewardPerIncrement, computeSyncParticipantReward} from "../util/syncCommittee.js"; import {sumTargetUnslashedBalanceIncrements} from "../util/targetUnslashedBalance.js"; import {getTotalSlashingsByIncrement} from "../epoch/processSlashings.js"; +import {EpochCacheMetrics} from "../metrics.js"; import {EffectiveBalanceIncrements, getEffectiveBalanceIncrementsWithLen} from "./effectiveBalanceIncrements.js"; -import {Index2PubkeyCache, PubkeyIndexMap, syncPubkeys} from "./pubkeyCache.js"; +import { + Index2PubkeyCache, + PubkeyIndexMap, + UnfinalizedPubkeyIndexMap, + syncPubkeys, + toMemoryEfficientHexStr, + PubkeyHex, + newUnfinalizedPubkeyIndexMap, +} from "./pubkeyCache.js"; import {BeaconStateAllForks, BeaconStateAltair, ShufflingGetter} from "./types.js"; import { computeSyncCommitteeCache, @@ -83,23 +94,31 @@ type ProposersDeferred = {computed: false; seed: Uint8Array} | {computed: true; export class EpochCache { config: BeaconConfig; /** - * Unique globally shared pubkey registry. There should only exist one for the entire application. + * Unique globally shared finalized pubkey registry. There should only exist one for the entire application. * * TODO: this is a hack, we need a safety mechanism in case a bad eth1 majority vote is in, * or handle non finalized data differently, or use an immutable.js structure for cheap copies - * Warning: may contain pubkeys that do not yet exist in the current state, but do in a later processed state. + * + * New: This would include only validators whose activation_eligibility_epoch != FAR_FUTURE_EPOCH and hence it is + * insert only. Validators could be 1) Active 2) In the activation queue 3) Initialized but pending queued * * $VALIDATOR_COUNT x 192 char String -> Number Map */ pubkey2index: PubkeyIndexMap; /** - * Unique globally shared pubkey registry. There should only exist one for the entire application. + * Unique globally shared finalized pubkey registry. There should only exist one for the entire application. * - * Warning: may contain indices that do not yet exist in the current state, but do in a later processed state. + * New: This would include only validators whose activation_eligibility_epoch != FAR_FUTURE_EPOCH and hence it is + * insert only. Validators could be 1) Active 2) In the activation queue 3) Initialized but pending queued * * $VALIDATOR_COUNT x BLST deserialized pubkey (Jacobian coordinates) */ index2pubkey: Index2PubkeyCache; + /** + * Unique pubkey registry shared in the same fork. There should only exist one for the fork. + */ + unfinalizedPubkey2index: UnfinalizedPubkeyIndexMap; + /** * Indexes of the block proposers for the current epoch. * @@ -199,11 +218,21 @@ export class EpochCache { // TODO: Helper stats epoch: Epoch; syncPeriod: SyncPeriod; + /** + * state.validators.length of every state at epoch boundary + * They are saved in increasing order of epoch. + * The first validator length in the list corresponds to the state AFTER the latest finalized checkpoint state. ie. state.finalizedCheckpoint.epoch - 1 + * The last validator length corresponds to the latest epoch state ie. this.epoch + * eg. latest epoch = 105, latest finalized cp state epoch = 102 + * then the list will be (in terms of epoch) [103, 104, 105] + */ + private historicalValidatorLengths: immutable.List; constructor(data: { config: BeaconConfig; pubkey2index: PubkeyIndexMap; index2pubkey: Index2PubkeyCache; + unfinalizedPubkey2index: UnfinalizedPubkeyIndexMap; proposers: number[]; proposersPrevEpoch: number[] | null; proposersNextEpoch: ProposersDeferred; @@ -226,10 +255,12 @@ export class EpochCache { nextSyncCommitteeIndexed: SyncCommitteeCache; epoch: Epoch; syncPeriod: SyncPeriod; + historialValidatorLengths: immutable.List; }) { this.config = data.config; this.pubkey2index = data.pubkey2index; this.index2pubkey = data.index2pubkey; + this.unfinalizedPubkey2index = data.unfinalizedPubkey2index; this.proposers = data.proposers; this.proposersPrevEpoch = data.proposersPrevEpoch; this.proposersNextEpoch = data.proposersNextEpoch; @@ -252,11 +283,12 @@ export class EpochCache { this.nextSyncCommitteeIndexed = data.nextSyncCommitteeIndexed; this.epoch = data.epoch; this.syncPeriod = data.syncPeriod; + this.historicalValidatorLengths = data.historialValidatorLengths; } /** * Create an epoch cache - * @param validators cached validators that matches `state.validators` + * @param state a finalized beacon state. Passing in unfinalized state may cause unexpected behaviour eg. empty unfinalized cache * * SLOW CODE - 🐢 */ @@ -427,6 +459,8 @@ export class EpochCache { config, pubkey2index, index2pubkey, + // `createFromFinalizedState()` creates cache with empty unfinalizedPubkey2index. Be cautious to only pass in finalized state + unfinalizedPubkey2index: newUnfinalizedPubkeyIndexMap(), proposers, // On first epoch, set to null to prevent unnecessary work since this is only used for metrics proposersPrevEpoch: null, @@ -450,6 +484,7 @@ export class EpochCache { nextSyncCommitteeIndexed, epoch: currentEpoch, syncPeriod: computeSyncPeriodAtEpoch(currentEpoch), + historialValidatorLengths: immutable.List(), }); } @@ -465,6 +500,8 @@ export class EpochCache { // Common append-only structures shared with all states, no need to clone pubkey2index: this.pubkey2index, index2pubkey: this.index2pubkey, + // No need to clone this reference. On each mutation the `unfinalizedPubkey2index` reference is replaced, @see `addPubkey` + unfinalizedPubkey2index: this.unfinalizedPubkey2index, // Immutable data proposers: this.proposers, proposersPrevEpoch: this.proposersPrevEpoch, @@ -491,6 +528,7 @@ export class EpochCache { nextSyncCommitteeIndexed: this.nextSyncCommitteeIndexed, epoch: this.epoch, syncPeriod: this.syncPeriod, + historialValidatorLengths: this.historicalValidatorLengths, }); } @@ -501,6 +539,7 @@ export class EpochCache { afterProcessEpoch( state: BeaconStateAllForks, epochTransitionCache: { + indicesEligibleForActivationQueue: ValidatorIndex[]; nextEpochShufflingActiveValidatorIndices: ValidatorIndex[]; nextEpochTotalActiveBalanceByIncrement: number; } @@ -573,6 +612,25 @@ export class EpochCache { // ``` this.epoch = computeEpochAtSlot(state.slot); this.syncPeriod = computeSyncPeriodAtEpoch(this.epoch); + // ELECTRA Only: Add current cpState.validators.length + // Only keep validatorLength for epochs after finalized cpState.epoch + // eg. [100(epoch 1), 102(epoch 2)].push(104(epoch 3)), this.epoch = 3, finalized cp epoch = 1 + // We keep the last (3 - 1) items = [102, 104] + if (currEpoch >= this.config.ELECTRA_FORK_EPOCH) { + this.historicalValidatorLengths = this.historicalValidatorLengths.push(state.validators.length); + + // If number of validatorLengths we want to keep exceeds the current list size, it implies + // finalized checkpoint hasn't advanced, and no need to slice + const hasFinalizedCpAdvanced = + this.epoch - state.finalizedCheckpoint.epoch < this.historicalValidatorLengths.size; + + if (hasFinalizedCpAdvanced) { + // We use finalized cp epoch - this.epoch which is a negative number to keep the last n entries and discard the rest + this.historicalValidatorLengths = this.historicalValidatorLengths.slice( + state.finalizedCheckpoint.epoch - this.epoch + ); + } + } } beforeEpochTransition(): void { @@ -760,9 +818,75 @@ export class EpochCache { return isAggregatorFromCommitteeLength(committee.length, slotSignature); } + /** + * Return finalized pubkey given the validator index. + * Only finalized pubkey as we do not store unfinalized pubkey because no where in the spec has a + * need to make such enquiry + */ + getPubkey(index: ValidatorIndex): PublicKey | undefined { + return this.index2pubkey[index]; + } + + getValidatorIndex(pubkey: Uint8Array | PubkeyHex): ValidatorIndex | undefined { + if (this.isAfterElectra()) { + return this.pubkey2index.get(pubkey) ?? this.unfinalizedPubkey2index.get(toMemoryEfficientHexStr(pubkey)); + } else { + return this.pubkey2index.get(pubkey); + } + } + + /** + * + * Add unfinalized pubkeys + * + */ addPubkey(index: ValidatorIndex, pubkey: Uint8Array): void { + if (this.isAfterElectra()) { + this.addUnFinalizedPubkey(index, pubkey); + } else { + // deposit mechanism pre ELECTRA follows a safe distance with assumption + // that they are already canonical + this.addFinalizedPubkey(index, pubkey); + } + } + + addUnFinalizedPubkey(index: ValidatorIndex, pubkey: PubkeyHex | Uint8Array, metrics?: EpochCacheMetrics): void { + this.unfinalizedPubkey2index = this.unfinalizedPubkey2index.set(toMemoryEfficientHexStr(pubkey), index); + metrics?.newUnFinalizedPubkey.inc(); + } + + addFinalizedPubkeys(pubkeyMap: UnfinalizedPubkeyIndexMap, metrics?: EpochCacheMetrics): void { + pubkeyMap.forEach((index, pubkey) => this.addFinalizedPubkey(index, pubkey, metrics)); + } + + /** + * Add finalized validator index and pubkey into finalized cache. + * Since addFinalizedPubkey() primarily takes pubkeys from unfinalized cache, it can take pubkey hex string directly + */ + addFinalizedPubkey(index: ValidatorIndex, pubkey: PubkeyHex | Uint8Array, metrics?: EpochCacheMetrics): void { + const existingIndex = this.pubkey2index.get(pubkey); + + if (existingIndex !== undefined) { + if (existingIndex === index) { + // Repeated insert. + metrics?.finalizedPubkeyDuplicateInsert.inc(); + return; + } else { + // attempt to insert the same pubkey with different index, should never happen. + throw Error("inserted existing pubkey into finalizedPubkey2index cache with a different index"); + } + } + this.pubkey2index.set(pubkey, index); - this.index2pubkey[index] = bls.PublicKey.fromBytes(pubkey, CoordType.jacobian); // Optimize for aggregation + const pubkeyBytes = pubkey instanceof Uint8Array ? pubkey : fromHexString(pubkey); + this.index2pubkey[index] = bls.PublicKey.fromBytes(pubkeyBytes, CoordType.jacobian); + } + + /** + * Delete pubkeys from unfinalized cache + */ + deleteUnfinalizedPubkeys(pubkeys: Iterable): void { + this.unfinalizedPubkey2index = this.unfinalizedPubkey2index.deleteAll(pubkeys); } getShufflingAtSlot(slot: Slot): EpochShuffling { @@ -839,15 +963,53 @@ export class EpochCache { } effectiveBalanceIncrementsSet(index: number, effectiveBalance: number): void { - if (index >= this.effectiveBalanceIncrements.length) { - // Clone and extend effectiveBalanceIncrements + if (this.isAfterElectra()) { + // TODO: electra + // getting length and setting getEffectiveBalanceIncrementsByteLen is not fork safe + // so each time we add an index, we should new the Uint8Array to keep it forksafe + // one simple optimization could be to increment the length once per block rather + // on each add/set + // + // there could still be some unused length remaining from the prev ELECTRA padding + const newLength = + index >= this.effectiveBalanceIncrements.length ? index + 1 : this.effectiveBalanceIncrements.length; const effectiveBalanceIncrements = this.effectiveBalanceIncrements; - this.effectiveBalanceIncrements = new Uint8Array(getEffectiveBalanceIncrementsByteLen(index + 1)); + this.effectiveBalanceIncrements = new Uint8Array(newLength); this.effectiveBalanceIncrements.set(effectiveBalanceIncrements, 0); + } else { + if (index >= this.effectiveBalanceIncrements.length) { + // Clone and extend effectiveBalanceIncrements + const effectiveBalanceIncrements = this.effectiveBalanceIncrements; + this.effectiveBalanceIncrements = new Uint8Array(getEffectiveBalanceIncrementsByteLen(index + 1)); + this.effectiveBalanceIncrements.set(effectiveBalanceIncrements, 0); + } } this.effectiveBalanceIncrements[index] = Math.floor(effectiveBalance / EFFECTIVE_BALANCE_INCREMENT); } + + isAfterElectra(): boolean { + return this.epoch >= (this.config.ELECTRA_FORK_EPOCH ?? Infinity); + } + + getValidatorCountAtEpoch(targetEpoch: Epoch): number | undefined { + const currentEpoch = this.epoch; + + if (targetEpoch === currentEpoch) { + return this.historicalValidatorLengths.get(-1); + } + + // Attempt to get validator count from future epoch + if (targetEpoch > currentEpoch) { + return undefined; + } + + // targetEpoch is so far back that historicalValidatorLengths doesnt contain such info + if (targetEpoch < currentEpoch - this.historicalValidatorLengths.size + 1) { + return undefined; + } + return this.historicalValidatorLengths.get(targetEpoch - currentEpoch - 1); + } } function getEffectiveBalanceIncrementsByteLen(validatorCount: number): number { diff --git a/packages/state-transition/src/cache/pubkeyCache.ts b/packages/state-transition/src/cache/pubkeyCache.ts index fdc343ff4ec7..39041cc6ba1d 100644 --- a/packages/state-transition/src/cache/pubkeyCache.ts +++ b/packages/state-transition/src/cache/pubkeyCache.ts @@ -1,11 +1,19 @@ import {CoordType, PublicKey} from "@chainsafe/bls/types"; import bls from "@chainsafe/bls"; +import * as immutable from "immutable"; import {ValidatorIndex} from "@lodestar/types"; import {BeaconStateAllForks} from "./types.js"; export type Index2PubkeyCache = PublicKey[]; +/** + * OrderedMap preserves the order of entries in which they are `set()`. + * We assume `values()` yields validator indices in strictly increasing order + * as new validator indices are assigned in increasing order. + * EIP-6914 will break this assumption. + */ +export type UnfinalizedPubkeyIndexMap = immutable.Map; -type PubkeyHex = string; +export type PubkeyHex = string; /** * toHexString() creates hex strings via string concatenation, which are very memory inefficient. @@ -15,7 +23,7 @@ type PubkeyHex = string; * * See https://github.com/ChainSafe/lodestar/issues/3446 */ -function toMemoryEfficientHexStr(hex: Uint8Array | string): string { +export function toMemoryEfficientHexStr(hex: Uint8Array | string): string { if (typeof hex === "string") { if (hex.startsWith("0x")) { hex = hex.slice(2); @@ -26,6 +34,13 @@ function toMemoryEfficientHexStr(hex: Uint8Array | string): string { return Buffer.from(hex.buffer, hex.byteOffset, hex.byteLength).toString("hex"); } +/** + * A wrapper for calling immutable.js. To abstract the initialization of UnfinalizedPubkeyIndexMap + */ +export function newUnfinalizedPubkeyIndexMap(): UnfinalizedPubkeyIndexMap { + return immutable.Map(); +} + export class PubkeyIndexMap { // We don't really need the full pubkey. We could just use the first 20 bytes like an Ethereum address readonly map = new Map(); @@ -41,7 +56,7 @@ export class PubkeyIndexMap { return this.map.get(toMemoryEfficientHexStr(key)); } - set(key: Uint8Array, value: ValidatorIndex): void { + set(key: Uint8Array | PubkeyHex, value: ValidatorIndex): void { this.map.set(toMemoryEfficientHexStr(key), value); } } diff --git a/packages/state-transition/src/cache/stateCache.ts b/packages/state-transition/src/cache/stateCache.ts index 8b45152a3646..aaaaf62a48df 100644 --- a/packages/state-transition/src/cache/stateCache.ts +++ b/packages/state-transition/src/cache/stateCache.ts @@ -11,6 +11,7 @@ import { BeaconStateBellatrix, BeaconStateCapella, BeaconStateDeneb, + BeaconStateElectra, } from "./types.js"; import {RewardCache, createEmptyRewardCache} from "./rewardCache.js"; @@ -131,11 +132,13 @@ export type CachedBeaconStateAltair = CachedBeaconState; export type CachedBeaconStateBellatrix = CachedBeaconState; export type CachedBeaconStateCapella = CachedBeaconState; export type CachedBeaconStateDeneb = CachedBeaconState; +export type CachedBeaconStateElectra = CachedBeaconState; export type CachedBeaconStateAllForks = CachedBeaconState; export type CachedBeaconStateExecutions = CachedBeaconState; /** * Create CachedBeaconState computing a new EpochCache instance + * TODO ELECTRA: rename this to createFinalizedCachedBeaconState() as it's intended for finalized state only */ export function createCachedBeaconState( state: T, @@ -159,7 +162,7 @@ export function createCachedBeaconState( * Create a CachedBeaconState given a cached seed state and state bytes * This guarantees that the returned state shares the same tree with the seed state * Check loadState() api for more details - * // TODO: rename to loadUnfinalizedCachedBeaconState() due to EIP-6110 + * // TODO: rename to loadUnfinalizedCachedBeaconState() due to ELECTRA */ export function loadCachedBeaconState( cachedSeedState: T, diff --git a/packages/state-transition/src/cache/types.ts b/packages/state-transition/src/cache/types.ts index 39b1dbb4b45b..a865aa4f4183 100644 --- a/packages/state-transition/src/cache/types.ts +++ b/packages/state-transition/src/cache/types.ts @@ -7,6 +7,7 @@ export type BeaconStateAltair = CompositeViewDU; export type BeaconStateBellatrix = CompositeViewDU; export type BeaconStateCapella = CompositeViewDU; export type BeaconStateDeneb = CompositeViewDU; +export type BeaconStateElectra = CompositeViewDU; // Union at the TreeViewDU level // - Works well as function argument and as generic type for allForks functions @@ -18,8 +19,9 @@ export type BeaconStateAllForks = | BeaconStateAltair | BeaconStateBellatrix | BeaconStateCapella - | BeaconStateDeneb; + | BeaconStateDeneb + | BeaconStateElectra; -export type BeaconStateExecutions = BeaconStateBellatrix | BeaconStateCapella | BeaconStateDeneb; +export type BeaconStateExecutions = BeaconStateBellatrix | BeaconStateCapella | BeaconStateDeneb | BeaconStateElectra; export type ShufflingGetter = (shufflingEpoch: Epoch, dependentRoot: RootHex) => EpochShuffling | null; diff --git a/packages/state-transition/src/index.ts b/packages/state-transition/src/index.ts index 0ef460e784af..4ed801e3c490 100644 --- a/packages/state-transition/src/index.ts +++ b/packages/state-transition/src/index.ts @@ -11,6 +11,7 @@ export type { CachedBeaconStateBellatrix, CachedBeaconStateCapella, CachedBeaconStateDeneb, + CachedBeaconStateElectra, CachedBeaconStateAllForks, CachedBeaconStateExecutions, // Non-cached states @@ -19,6 +20,7 @@ export type { BeaconStateBellatrix, BeaconStateCapella, BeaconStateDeneb, + BeaconStateElectra, BeaconStateAllForks, BeaconStateExecutions, } from "./types.js"; @@ -42,7 +44,12 @@ export { export {type EpochTransitionCache, beforeProcessEpoch} from "./cache/epochTransitionCache.js"; // Aux data-structures -export {PubkeyIndexMap, type Index2PubkeyCache} from "./cache/pubkeyCache.js"; +export { + PubkeyIndexMap, + type Index2PubkeyCache, + type UnfinalizedPubkeyIndexMap, + newUnfinalizedPubkeyIndexMap, +} from "./cache/pubkeyCache.js"; export { type EffectiveBalanceIncrements, diff --git a/packages/state-transition/src/metrics.ts b/packages/state-transition/src/metrics.ts index 62062bbfc539..9478087df83f 100644 --- a/packages/state-transition/src/metrics.ts +++ b/packages/state-transition/src/metrics.ts @@ -24,6 +24,11 @@ export type BeaconStateTransitionMetrics = { registerValidatorStatuses: (currentEpoch: Epoch, statuses: AttesterStatus[], balances?: number[]) => void; }; +export type EpochCacheMetrics = { + finalizedPubkeyDuplicateInsert: Gauge; + newUnFinalizedPubkey: Gauge; +}; + export function onStateCloneMetrics( state: CachedBeaconStateAllForks, metrics: BeaconStateTransitionMetrics, diff --git a/packages/state-transition/src/signatureSets/attesterSlashings.ts b/packages/state-transition/src/signatureSets/attesterSlashings.ts index 370d48a7d2f0..6a985f84f64e 100644 --- a/packages/state-transition/src/signatureSets/attesterSlashings.ts +++ b/packages/state-transition/src/signatureSets/attesterSlashings.ts @@ -27,13 +27,12 @@ export function getIndexedAttestationBigintSignatureSet( state: CachedBeaconStateAllForks, indexedAttestation: phase0.IndexedAttestationBigint ): ISignatureSet { - const {index2pubkey} = state.epochCtx; const slot = computeStartSlotAtEpoch(Number(indexedAttestation.data.target.epoch as bigint)); const domain = state.config.getDomain(state.slot, DOMAIN_BEACON_ATTESTER, slot); return { type: SignatureSetType.aggregate, - pubkeys: indexedAttestation.attestingIndices.map((i) => index2pubkey[i]), + pubkeys: indexedAttestation.attestingIndices.map((i) => state.epochCtx.index2pubkey[i]), signingRoot: computeSigningRoot(ssz.phase0.AttestationDataBigint, indexedAttestation.data, domain), signature: indexedAttestation.signature, }; diff --git a/packages/state-transition/src/slot/index.ts b/packages/state-transition/src/slot/index.ts index 6c4add1d1230..b05bd7ac93f2 100644 --- a/packages/state-transition/src/slot/index.ts +++ b/packages/state-transition/src/slot/index.ts @@ -7,6 +7,7 @@ export {upgradeStateToAltair} from "./upgradeStateToAltair.js"; export {upgradeStateToBellatrix} from "./upgradeStateToBellatrix.js"; export {upgradeStateToCapella} from "./upgradeStateToCapella.js"; export {upgradeStateToDeneb} from "./upgradeStateToDeneb.js"; +export {upgradeStateToElectra} from "./upgradeStateToElectra.js"; /** * Dial state to next slot. Common for all forks diff --git a/packages/state-transition/src/slot/upgradeStateToElectra.ts b/packages/state-transition/src/slot/upgradeStateToElectra.ts new file mode 100644 index 000000000000..19cac8811f77 --- /dev/null +++ b/packages/state-transition/src/slot/upgradeStateToElectra.ts @@ -0,0 +1,33 @@ +import {ssz} from "@lodestar/types"; +import {UNSET_DEPOSIT_RECEIPTS_START_INDEX} from "@lodestar/params"; +import {CachedBeaconStateDeneb} from "../types.js"; +import {CachedBeaconStateElectra, getCachedBeaconState} from "../cache/stateCache.js"; + +/** + * Upgrade a state from Capella to Deneb. + */ +export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): CachedBeaconStateElectra { + const {config} = stateDeneb; + + const stateElectraNode = ssz.deneb.BeaconState.commitViewDU(stateDeneb); + const stateElectraView = ssz.electra.BeaconState.getViewDU(stateElectraNode); + + const stateElectra = getCachedBeaconState(stateElectraView, stateDeneb); + + stateElectra.fork = ssz.phase0.Fork.toViewDU({ + previousVersion: stateDeneb.fork.currentVersion, + currentVersion: config.ELECTRA_FORK_VERSION, + epoch: stateDeneb.epochCtx.epoch, + }); + + // latestExecutionPayloadHeader's depositReceiptsRoot set to zeros by default + // default value of depositReceiptsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX + stateElectra.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + + // Commit new added fields ViewDU to the root node + stateElectra.commit(); + // Clear cache to ensure the cache of deneb fields is not used by new ELECTRA fields + stateElectra["clearCache"](); + + return stateElectra; +} diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index b3f3b41eb865..049f04dce287 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -9,6 +9,7 @@ import { CachedBeaconStateAltair, CachedBeaconStateBellatrix, CachedBeaconStateCapella, + CachedBeaconStateDeneb, } from "./types.js"; import {computeEpochAtSlot} from "./util/index.js"; import {verifyProposerSignature} from "./signatureSets/index.js"; @@ -18,6 +19,7 @@ import { upgradeStateToBellatrix, upgradeStateToCapella, upgradeStateToDeneb, + upgradeStateToElectra, } from "./slot/index.js"; import {processBlock} from "./block/index.js"; import {EpochTransitionStep, processEpoch} from "./epoch/index.js"; @@ -230,6 +232,9 @@ function processSlotsWithTransientCache( if (stateSlot === config.DENEB_FORK_EPOCH) { postState = upgradeStateToDeneb(postState as CachedBeaconStateCapella) as CachedBeaconStateAllForks; } + if (stateSlot === config.ELECTRA_FORK_EPOCH) { + postState = upgradeStateToElectra(postState as CachedBeaconStateDeneb) as CachedBeaconStateAllForks; + } } else { postState.slot++; } diff --git a/packages/state-transition/src/types.ts b/packages/state-transition/src/types.ts index 6b6b1f6260b2..d3a1ed69a7a9 100644 --- a/packages/state-transition/src/types.ts +++ b/packages/state-transition/src/types.ts @@ -9,6 +9,7 @@ export type { CachedBeaconStateBellatrix, CachedBeaconStateCapella, CachedBeaconStateDeneb, + CachedBeaconStateElectra, } from "./cache/stateCache.js"; export type { @@ -19,4 +20,5 @@ export type { BeaconStateBellatrix, BeaconStateCapella, BeaconStateDeneb, + BeaconStateElectra, } from "./cache/types.js"; diff --git a/packages/state-transition/src/util/deposit.ts b/packages/state-transition/src/util/deposit.ts new file mode 100644 index 000000000000..493099fdd982 --- /dev/null +++ b/packages/state-transition/src/util/deposit.ts @@ -0,0 +1,24 @@ +import {ForkSeq, MAX_DEPOSITS} from "@lodestar/params"; +import {UintNum64, phase0} from "@lodestar/types"; +import {CachedBeaconStateAllForks, CachedBeaconStateElectra} from "../types.js"; + +export function getEth1DepositCount(state: CachedBeaconStateAllForks, eth1Data?: phase0.Eth1Data): UintNum64 { + const eth1DataToUse = eth1Data ?? state.eth1Data; + if (state.config.getForkSeq(state.slot) >= ForkSeq.electra) { + const electraState = state as CachedBeaconStateElectra; + // eth1DataIndexLimit = min(UintNum64, UintBn64) can be safely casted as UintNum64 + // since the result lies within upper and lower bound of UintNum64 + const eth1DataIndexLimit: UintNum64 = + eth1DataToUse.depositCount < electraState.depositReceiptsStartIndex + ? eth1DataToUse.depositCount + : Number(electraState.depositReceiptsStartIndex); + + if (state.eth1DepositIndex < eth1DataIndexLimit) { + return Math.min(MAX_DEPOSITS, eth1DataIndexLimit - state.eth1DepositIndex); + } else { + return 0; + } + } else { + return Math.min(MAX_DEPOSITS, eth1DataToUse.depositCount - state.eth1DepositIndex); + } +} diff --git a/packages/state-transition/src/util/execution.ts b/packages/state-transition/src/util/execution.ts index 7ac4da4aeecb..b7923fd9f807 100644 --- a/packages/state-transition/src/util/execution.ts +++ b/packages/state-transition/src/util/execution.ts @@ -1,4 +1,4 @@ -import {allForks, bellatrix, capella, deneb, isBlindedBeaconBlockBody, ssz} from "@lodestar/types"; +import {allForks, bellatrix, capella, deneb, electra, isBlindedBeaconBlockBody, ssz} from "@lodestar/types"; import {ForkSeq} from "@lodestar/params"; import { @@ -170,5 +170,10 @@ export function executionPayloadToPayloadHeader( ).excessBlobGas; } + if (fork >= ForkSeq.electra) { + (bellatrixPayloadFields as electra.ExecutionPayloadHeader).depositReceiptsRoot = + ssz.electra.DepositReceipts.hashTreeRoot((payload as electra.ExecutionPayload).depositReceipts); + } + return bellatrixPayloadFields; } diff --git a/packages/state-transition/src/util/genesis.ts b/packages/state-transition/src/util/genesis.ts index 1041c33d0eb3..6c700436d7f6 100644 --- a/packages/state-transition/src/util/genesis.ts +++ b/packages/state-transition/src/util/genesis.ts @@ -7,6 +7,7 @@ import { GENESIS_EPOCH, GENESIS_SLOT, MAX_EFFECTIVE_BALANCE, + UNSET_DEPOSIT_RECEIPTS_START_INDEX, } from "@lodestar/params"; import {Bytes32, phase0, Root, ssz, TimeSeconds} from "@lodestar/types"; @@ -214,6 +215,7 @@ export function initializeBeaconStateFromEth1( | typeof ssz.bellatrix.ExecutionPayloadHeader | typeof ssz.capella.ExecutionPayloadHeader | typeof ssz.deneb.ExecutionPayloadHeader + | typeof ssz.electra.ExecutionPayloadHeader > ): CachedBeaconStateAllForks { const stateView = getGenesisBeaconState( @@ -284,6 +286,16 @@ export function initializeBeaconStateFromEth1( ssz.deneb.ExecutionPayloadHeader.defaultViewDU(); } + if (GENESIS_SLOT >= config.ELECTRA_FORK_EPOCH) { + const stateElectra = state as CompositeViewDU; + stateElectra.fork.previousVersion = config.ELECTRA_FORK_VERSION; + stateElectra.fork.currentVersion = config.ELECTRA_FORK_VERSION; + stateElectra.latestExecutionPayloadHeader = + (executionPayloadHeader as CompositeViewDU) ?? + ssz.electra.ExecutionPayloadHeader.defaultViewDU(); + stateElectra.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + } + state.commit(); return state; diff --git a/packages/state-transition/src/util/index.ts b/packages/state-transition/src/util/index.ts index 3f2e91da9a77..ba998f65b254 100644 --- a/packages/state-transition/src/util/index.ts +++ b/packages/state-transition/src/util/index.ts @@ -23,3 +23,4 @@ export * from "./slot.js"; export * from "./syncCommittee.js"; export * from "./validator.js"; export * from "./weakSubjectivity.js"; +export * from "./deposit.js"; diff --git a/packages/state-transition/test/unit/cachedBeaconState.test.ts b/packages/state-transition/test/unit/cachedBeaconState.test.ts index 2891cd3e6216..9b05b8947f73 100644 --- a/packages/state-transition/test/unit/cachedBeaconState.test.ts +++ b/packages/state-transition/test/unit/cachedBeaconState.test.ts @@ -1,8 +1,9 @@ +import {fromHexString} from "@chainsafe/ssz"; import {describe, it, expect} from "vitest"; import {Epoch, ssz, RootHex} from "@lodestar/types"; import {toHexString} from "@lodestar/utils"; import {config as defaultConfig} from "@lodestar/config/default"; -import {createBeaconConfig} from "@lodestar/config"; +import {createBeaconConfig, createChainForkConfig} from "@lodestar/config"; import {createCachedBeaconStateTest} from "../utils/state.js"; import {PubkeyIndexMap} from "../../src/cache/pubkeyCache.js"; import {createCachedBeaconState, loadCachedBeaconState} from "../../src/cache/stateCache.js"; @@ -28,6 +29,65 @@ describe("CachedBeaconState", () => { expect(state2.epochCtx.epoch).toBe(0); }); + it("Clone and mutate cache pre-Electra", () => { + const stateView = ssz.altair.BeaconState.defaultViewDU(); + const state1 = createCachedBeaconStateTest(stateView); + + const pubkey1 = fromHexString( + "0x84105a985058fc8740a48bf1ede9d223ef09e8c6b1735ba0a55cf4a9ff2ff92376b778798365e488dab07a652eb04576" + ); + const index1 = 123; + const pubkey2 = fromHexString( + "0xa41726266b1d83ef609d759ba7796d54cfe549154e01e4730a3378309bc81a7638140d7e184b33593c072595f23f032d" + ); + const index2 = 456; + + state1.epochCtx.addPubkey(index1, pubkey1); + + const state2 = state1.clone(); + state2.epochCtx.addPubkey(index2, pubkey2); + + expect(state1.epochCtx.getValidatorIndex(pubkey1)).toBe(index1); + expect(state2.epochCtx.getValidatorIndex(pubkey1)).toBe(index1); + expect(state1.epochCtx.getValidatorIndex(pubkey2)).toBe(index2); + expect(state2.epochCtx.getValidatorIndex(pubkey2)).toBe(index2); + }); + + /* eslint-disable @typescript-eslint/naming-convention */ + it("Clone and mutate cache post-Electra", () => { + const stateView = ssz.electra.BeaconState.defaultViewDU(); + const state1 = createCachedBeaconStateTest( + stateView, + createChainForkConfig({ + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: 0, + }), + {skipSyncCommitteeCache: true, skipSyncPubkeys: true} + ); + + const pubkey1 = fromHexString( + "0x84105a985058fc8740a48bf1ede9d223ef09e8c6b1735ba0a55cf4a9ff2ff92376b778798365e488dab07a652eb04576" + ); + const index1 = 123; + const pubkey2 = fromHexString( + "0xa41726266b1d83ef609d759ba7796d54cfe549154e01e4730a3378309bc81a7638140d7e184b33593c072595f23f032d" + ); + const index2 = 456; + + state1.epochCtx.addPubkey(index1, pubkey1); + + const state2 = state1.clone(); + state2.epochCtx.addPubkey(index2, pubkey2); + + expect(state1.epochCtx.getValidatorIndex(pubkey1)).toBe(index1); + expect(state2.epochCtx.getValidatorIndex(pubkey1)).toBe(index1); + expect(state1.epochCtx.getValidatorIndex(pubkey2)).toBe(undefined); + expect(state2.epochCtx.getValidatorIndex(pubkey2)).toBe(index2); + }); + it("Auto-commit on hashTreeRoot", () => { // Use Checkpoint instead of BeaconState to speed up the test const cp1 = ssz.phase0.Checkpoint.defaultViewDU(); diff --git a/packages/state-transition/test/unit/upgradeState.test.ts b/packages/state-transition/test/unit/upgradeState.test.ts index 75ba415c1bea..df9b052542f9 100644 --- a/packages/state-transition/test/unit/upgradeState.test.ts +++ b/packages/state-transition/test/unit/upgradeState.test.ts @@ -5,6 +5,7 @@ import {createBeaconConfig, ChainForkConfig, createChainForkConfig} from "@lodes import {config as chainConfig} from "@lodestar/config/default"; import {upgradeStateToDeneb} from "../../src/slot/upgradeStateToDeneb.js"; +import {upgradeStateToElectra} from "../../src/slot/upgradeStateToElectra.js"; import {createCachedBeaconState} from "../../src/cache/stateCache.js"; import {PubkeyIndexMap} from "../../src/cache/pubkeyCache.js"; @@ -24,6 +25,21 @@ describe("upgradeState", () => { const newState = upgradeStateToDeneb(stateView); expect(() => newState.toValue()).not.toThrow(); }); + it("upgradeStateToElectra", () => { + const denebState = ssz.deneb.BeaconState.defaultViewDU(); + const config = getConfig(ForkName.deneb); + const stateView = createCachedBeaconState( + denebState, + { + config: createBeaconConfig(config, denebState.genesisValidatorsRoot), + pubkey2index: new PubkeyIndexMap(), + index2pubkey: [], + }, + {skipSyncCommitteeCache: true} + ); + const newState = upgradeStateToElectra(stateView); + expect(() => newState.toValue()).not.toThrow(); + }); }); const ZERO_HASH = Buffer.alloc(32, 0); diff --git a/packages/state-transition/test/unit/util/deposit.test.ts b/packages/state-transition/test/unit/util/deposit.test.ts new file mode 100644 index 000000000000..e8f7f7a86af8 --- /dev/null +++ b/packages/state-transition/test/unit/util/deposit.test.ts @@ -0,0 +1,99 @@ +import {describe, it, expect} from "vitest"; +import {ssz} from "@lodestar/types"; +import {createChainForkConfig} from "@lodestar/config"; +import {MAX_DEPOSITS} from "@lodestar/params"; +import {CachedBeaconStateElectra, getEth1DepositCount} from "../../../src/index.js"; +import {createCachedBeaconStateTest} from "../../utils/state.js"; + +describe("getEth1DepositCount", () => { + it("Pre Electra", () => { + const stateView = ssz.altair.BeaconState.defaultViewDU(); + const preElectraState = createCachedBeaconStateTest(stateView); + + if (preElectraState.epochCtx.isAfterElectra()) { + throw Error("Not a pre-Electra state"); + } + + preElectraState.eth1Data.depositCount = 123; + + // 1. Should get less than MAX_DEPOSIT + preElectraState.eth1DepositIndex = 120; + expect(getEth1DepositCount(preElectraState)).toBe(3); + + // 2. Should get MAX_DEPOSIT + preElectraState.eth1DepositIndex = 100; + expect(getEth1DepositCount(preElectraState)).toBe(MAX_DEPOSITS); + }); + it("Post Electra with eth1 deposit", () => { + const stateView = ssz.electra.BeaconState.defaultViewDU(); + const postElectraState = createCachedBeaconStateTest( + stateView, + createChainForkConfig({ + /* eslint-disable @typescript-eslint/naming-convention */ + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: 0, + }), + {skipSyncCommitteeCache: true, skipSyncPubkeys: true} + ) as CachedBeaconStateElectra; + + if (!postElectraState.epochCtx.isAfterElectra()) { + throw Error("Not a post-Electra state"); + } + + postElectraState.depositReceiptsStartIndex = 1000n; + postElectraState.eth1Data.depositCount = 995; + + // 1. Should get less than MAX_DEPOSIT + postElectraState.eth1DepositIndex = 990; + expect(getEth1DepositCount(postElectraState)).toBe(5); + + // 2. Should get MAX_DEPOSIT + postElectraState.eth1DepositIndex = 100; + expect(getEth1DepositCount(postElectraState)).toBe(MAX_DEPOSITS); + + // 3. Should be 0 + postElectraState.eth1DepositIndex = 1000; + expect(getEth1DepositCount(postElectraState)).toBe(0); + }); + it("Post Electra without eth1 deposit", () => { + const stateView = ssz.electra.BeaconState.defaultViewDU(); + const postElectraState = createCachedBeaconStateTest( + stateView, + createChainForkConfig({ + /* eslint-disable @typescript-eslint/naming-convention */ + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: 0, + }), + {skipSyncCommitteeCache: true, skipSyncPubkeys: true} + ) as CachedBeaconStateElectra; + + if (!postElectraState.epochCtx.isAfterElectra()) { + throw Error("Not a post-Electra state"); + } + + postElectraState.depositReceiptsStartIndex = 1000n; + postElectraState.eth1Data.depositCount = 1005; + + // Before eth1DepositIndex reaching the start index + // 1. Should get less than MAX_DEPOSIT + postElectraState.eth1DepositIndex = 990; + expect(getEth1DepositCount(postElectraState)).toBe(10); + + // 2. Should get MAX_DEPOSIT + postElectraState.eth1DepositIndex = 983; + expect(getEth1DepositCount(postElectraState)).toBe(MAX_DEPOSITS); + + // After eth1DepositIndex reaching the start index + // 1. Should be 0 + postElectraState.eth1DepositIndex = 1000; + expect(getEth1DepositCount(postElectraState)).toBe(0); + postElectraState.eth1DepositIndex = 1003; + expect(getEth1DepositCount(postElectraState)).toBe(0); + }); +}); diff --git a/packages/types/package.json b/packages/types/package.json index b892e6f0285a..920a490e8205 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -32,6 +32,9 @@ "./deneb": { "import": "./lib/deneb/index.js" }, + "./electra": { + "import": "./lib/electra/index.js" + }, "./phase0": { "import": "./lib/phase0/index.js" } diff --git a/packages/types/src/allForks/sszTypes.ts b/packages/types/src/allForks/sszTypes.ts index 6030215ac8ca..84c6bb86ce5f 100644 --- a/packages/types/src/allForks/sszTypes.ts +++ b/packages/types/src/allForks/sszTypes.ts @@ -192,7 +192,7 @@ export const allForksBlobs = { ExecutionPayloadAndBlobsBundle: deneb.ExecutionPayloadAndBlobsBundle, }, electra: { - BlobSidecar: electra.BlobSidecar, + BlobSidecar: deneb.BlobSidecar, ExecutionPayloadAndBlobsBundle: electra.ExecutionPayloadAndBlobsBundle, }, }; diff --git a/packages/types/src/allForks/types.ts b/packages/types/src/allForks/types.ts index 59768a5a3308..416bb051065a 100644 --- a/packages/types/src/allForks/types.ts +++ b/packages/types/src/allForks/types.ts @@ -4,12 +4,14 @@ import {ts as altair} from "../altair/index.js"; import {ts as bellatrix} from "../bellatrix/index.js"; import {ts as capella} from "../capella/index.js"; import {ts as deneb} from "../deneb/index.js"; +import {ts as electra} from "../electra/index.js"; import {ssz as phase0Ssz} from "../phase0/index.js"; import {ssz as altairSsz} from "../altair/index.js"; import {ssz as bellatrixSsz} from "../bellatrix/index.js"; import {ssz as capellaSsz} from "../capella/index.js"; import {ssz as denebSsz} from "../deneb/index.js"; +import {ssz as electraSsz} from "../electra/index.js"; // Re-export union types for types that are _known_ to differ @@ -18,47 +20,66 @@ export type BeaconBlockBody = | altair.BeaconBlockBody | bellatrix.BeaconBlockBody | capella.BeaconBlockBody - | deneb.BeaconBlockBody; + | deneb.BeaconBlockBody + | electra.BeaconBlockBody; export type BeaconBlock = | phase0.BeaconBlock | altair.BeaconBlock | bellatrix.BeaconBlock | capella.BeaconBlock - | deneb.BeaconBlock; + | deneb.BeaconBlock + | electra.BeaconBlock; export type SignedBeaconBlock = | phase0.SignedBeaconBlock | altair.SignedBeaconBlock | bellatrix.SignedBeaconBlock | capella.SignedBeaconBlock - | deneb.SignedBeaconBlock; + | deneb.SignedBeaconBlock + | electra.SignedBeaconBlock; export type BeaconState = | phase0.BeaconState | altair.BeaconState | bellatrix.BeaconState | capella.BeaconState - | deneb.BeaconState; + | deneb.BeaconState + | electra.BeaconState; export type Metadata = phase0.Metadata | altair.Metadata; // For easy reference in the assemble block for building payloads -export type ExecutionBlockBody = bellatrix.BeaconBlockBody | capella.BeaconBlockBody | deneb.BeaconBlockBody; +export type ExecutionBlockBody = + | bellatrix.BeaconBlockBody + | capella.BeaconBlockBody + | deneb.BeaconBlockBody + | electra.BeaconBlockBody; // These two additional types will also change bellatrix forward -export type ExecutionPayload = bellatrix.ExecutionPayload | capella.ExecutionPayload | deneb.ExecutionPayload; +export type ExecutionPayload = + | bellatrix.ExecutionPayload + | capella.ExecutionPayload + | deneb.ExecutionPayload + | electra.ExecutionPayload; export type ExecutionPayloadHeader = | bellatrix.ExecutionPayloadHeader | capella.ExecutionPayloadHeader - | deneb.ExecutionPayloadHeader; + | deneb.ExecutionPayloadHeader + | electra.ExecutionPayloadHeader; // Blinded types that will change across forks export type BlindedBeaconBlockBody = | bellatrix.BlindedBeaconBlockBody | capella.BlindedBeaconBlockBody - | deneb.BlindedBeaconBlockBody; -export type BlindedBeaconBlock = bellatrix.BlindedBeaconBlock | capella.BlindedBeaconBlock | deneb.BlindedBeaconBlock; + | deneb.BlindedBeaconBlockBody + | electra.BlindedBeaconBlockBody; +export type BlindedBeaconBlock = + | bellatrix.BlindedBeaconBlock + | capella.BlindedBeaconBlock + | deneb.BlindedBeaconBlock + | electra.BlindedBeaconBlock; export type SignedBlindedBeaconBlock = | bellatrix.SignedBlindedBeaconBlock | capella.SignedBlindedBeaconBlock - | deneb.SignedBlindedBeaconBlock; + | deneb.SignedBlindedBeaconBlock + | electra.SignedBlindedBeaconBlock; // Full or blinded types export type FullOrBlindedExecutionPayload = @@ -80,30 +101,52 @@ export type SignedBeaconBlockOrContents = SignedBeaconBlock | SignedBlockContent export type FullOrBlindedBeaconBlockOrContents = BeaconBlockOrContents | BlindedBeaconBlock; -export type BuilderBid = bellatrix.BuilderBid | capella.BuilderBid | deneb.BuilderBid; -export type SignedBuilderBid = bellatrix.SignedBuilderBid | capella.SignedBuilderBid | deneb.SignedBuilderBid; -export type ExecutionPayloadAndBlobsBundle = deneb.ExecutionPayloadAndBlobsBundle; +export type BuilderBid = bellatrix.BuilderBid | capella.BuilderBid | deneb.BuilderBid | electra.BuilderBid; +export type SignedBuilderBid = + | bellatrix.SignedBuilderBid + | capella.SignedBuilderBid + | deneb.SignedBuilderBid + | electra.SignedBuilderBid; +export type ExecutionPayloadAndBlobsBundle = + | deneb.ExecutionPayloadAndBlobsBundle + | electra.ExecutionPayloadAndBlobsBundle; -export type LightClientHeader = altair.LightClientHeader | capella.LightClientHeader | deneb.LightClientHeader; +export type LightClientHeader = + | altair.LightClientHeader + | capella.LightClientHeader + | deneb.LightClientHeader + | electra.LightClientHeader; export type LightClientBootstrap = | altair.LightClientBootstrap | capella.LightClientBootstrap - | deneb.LightClientBootstrap; -export type LightClientUpdate = altair.LightClientUpdate | capella.LightClientUpdate | deneb.LightClientUpdate; + | deneb.LightClientBootstrap + | electra.LightClientBootstrap; +export type LightClientUpdate = + | altair.LightClientUpdate + | capella.LightClientUpdate + | deneb.LightClientUpdate + | electra.LightClientUpdate; export type LightClientFinalityUpdate = | altair.LightClientFinalityUpdate | capella.LightClientFinalityUpdate - | deneb.LightClientFinalityUpdate; + | deneb.LightClientFinalityUpdate + | electra.LightClientFinalityUpdate; export type LightClientOptimisticUpdate = | altair.LightClientOptimisticUpdate | capella.LightClientOptimisticUpdate - | deneb.LightClientOptimisticUpdate; -export type LightClientStore = altair.LightClientStore | capella.LightClientStore | deneb.LightClientStore; + | deneb.LightClientOptimisticUpdate + | electra.LightClientOptimisticUpdate; +export type LightClientStore = + | altair.LightClientStore + | capella.LightClientStore + | deneb.LightClientStore + | electra.LightClientStore; export type SSEPayloadAttributes = | bellatrix.SSEPayloadAttributes | capella.SSEPayloadAttributes - | deneb.SSEPayloadAttributes; + | deneb.SSEPayloadAttributes + | electra.SSEPayloadAttributes; /** * Types known to change between forks @@ -128,7 +171,12 @@ export type AllForksBlindedTypes = { }; export type AllForksLightClient = { - BeaconBlock: altair.BeaconBlock | bellatrix.BeaconBlock | capella.BeaconBlock | deneb.BeaconBlock; + BeaconBlock: + | altair.BeaconBlock + | bellatrix.BeaconBlock + | capella.BeaconBlock + | deneb.BeaconBlock + | electra.BeaconBlock; LightClientHeader: LightClientHeader; LightClientBootstrap: LightClientBootstrap; LightClientUpdate: LightClientUpdate; @@ -138,8 +186,12 @@ export type AllForksLightClient = { }; export type AllForksExecution = { - BeaconBlock: bellatrix.BeaconBlock | capella.BeaconBlock | deneb.BeaconBlock; - BeaconBlockBody: bellatrix.BeaconBlockBody | capella.BeaconBlockBody | deneb.BeaconBlockBody; + BeaconBlock: bellatrix.BeaconBlock | capella.BeaconBlock | deneb.BeaconBlock | electra.BeaconBlock; + BeaconBlockBody: + | bellatrix.BeaconBlockBody + | capella.BeaconBlockBody + | deneb.BeaconBlockBody + | electra.BeaconBlockBody; }; /** @@ -178,6 +230,7 @@ export type AllForksSSZTypes = { | typeof bellatrixSsz.BeaconBlockBody | typeof capellaSsz.BeaconBlockBody | typeof denebSsz.BeaconBlockBody + | typeof electraSsz.BeaconBlockBody >; BeaconBlock: AllForksTypeOf< | typeof phase0Ssz.BeaconBlock @@ -185,6 +238,7 @@ export type AllForksSSZTypes = { | typeof bellatrixSsz.BeaconBlock | typeof capellaSsz.BeaconBlock | typeof denebSsz.BeaconBlock + | typeof electraSsz.BeaconBlock >; SignedBeaconBlock: AllForksTypeOf< | typeof phase0Ssz.SignedBeaconBlock @@ -192,6 +246,7 @@ export type AllForksSSZTypes = { | typeof bellatrixSsz.SignedBeaconBlock | typeof capellaSsz.SignedBeaconBlock | typeof denebSsz.SignedBeaconBlock + | typeof electraSsz.SignedBeaconBlock >; BeaconState: AllForksTypeOf< | typeof phase0Ssz.BeaconState @@ -199,41 +254,65 @@ export type AllForksSSZTypes = { | typeof bellatrixSsz.BeaconState | typeof capellaSsz.BeaconState | typeof denebSsz.BeaconState + | typeof electraSsz.BeaconState >; Metadata: AllForksTypeOf; }; export type AllForksExecutionSSZTypes = { BeaconBlockBody: AllForksTypeOf< - typeof bellatrixSsz.BeaconBlockBody | typeof capellaSsz.BeaconBlockBody | typeof denebSsz.BeaconBlockBody + | typeof bellatrixSsz.BeaconBlockBody + | typeof capellaSsz.BeaconBlockBody + | typeof denebSsz.BeaconBlockBody + | typeof electraSsz.BeaconBlockBody >; BeaconBlock: AllForksTypeOf< - typeof bellatrixSsz.BeaconBlock | typeof capellaSsz.BeaconBlock | typeof denebSsz.BeaconBlock + | typeof bellatrixSsz.BeaconBlock + | typeof capellaSsz.BeaconBlock + | typeof denebSsz.BeaconBlock + | typeof electraSsz.BeaconBlock >; SignedBeaconBlock: AllForksTypeOf< - typeof bellatrixSsz.SignedBeaconBlock | typeof capellaSsz.SignedBeaconBlock | typeof denebSsz.SignedBeaconBlock + | typeof bellatrixSsz.SignedBeaconBlock + | typeof capellaSsz.SignedBeaconBlock + | typeof denebSsz.SignedBeaconBlock + | typeof electraSsz.SignedBeaconBlock >; BeaconState: AllForksTypeOf< - typeof bellatrixSsz.BeaconState | typeof capellaSsz.BeaconState | typeof denebSsz.BeaconState + | typeof bellatrixSsz.BeaconState + | typeof capellaSsz.BeaconState + | typeof denebSsz.BeaconState + | typeof electraSsz.BeaconState >; ExecutionPayload: AllForksTypeOf< - typeof bellatrixSsz.ExecutionPayload | typeof capellaSsz.ExecutionPayload | typeof denebSsz.ExecutionPayload + | typeof bellatrixSsz.ExecutionPayload + | typeof capellaSsz.ExecutionPayload + | typeof denebSsz.ExecutionPayload + | typeof electraSsz.ExecutionPayload >; ExecutionPayloadHeader: AllForksTypeOf< | typeof bellatrixSsz.ExecutionPayloadHeader | typeof capellaSsz.ExecutionPayloadHeader | typeof denebSsz.ExecutionPayloadHeader + | typeof electraSsz.ExecutionPayloadHeader >; BuilderBid: AllForksTypeOf< - typeof bellatrixSsz.BuilderBid | typeof capellaSsz.BuilderBid | typeof denebSsz.BuilderBid + | typeof bellatrixSsz.BuilderBid + | typeof capellaSsz.BuilderBid + | typeof denebSsz.BuilderBid + | typeof electraSsz.BuilderBid >; SignedBuilderBid: AllForksTypeOf< - typeof bellatrixSsz.SignedBuilderBid | typeof capellaSsz.SignedBuilderBid | typeof denebSsz.SignedBuilderBid + | typeof bellatrixSsz.SignedBuilderBid + | typeof capellaSsz.SignedBuilderBid + | typeof denebSsz.SignedBuilderBid + | typeof electraSsz.SignedBuilderBid >; SSEPayloadAttributes: AllForksTypeOf< | typeof bellatrixSsz.SSEPayloadAttributes | typeof capellaSsz.SSEPayloadAttributes | typeof denebSsz.SSEPayloadAttributes + | typeof electraSsz.SSEPayloadAttributes >; }; @@ -242,14 +321,19 @@ export type AllForksBlindedSSZTypes = { | typeof bellatrixSsz.BlindedBeaconBlockBody | typeof capellaSsz.BlindedBeaconBlock | typeof denebSsz.BlindedBeaconBlock + | typeof electraSsz.BlindedBeaconBlock >; BeaconBlock: AllForksTypeOf< - typeof bellatrixSsz.BlindedBeaconBlock | typeof capellaSsz.BlindedBeaconBlock | typeof denebSsz.BlindedBeaconBlock + | typeof bellatrixSsz.BlindedBeaconBlock + | typeof capellaSsz.BlindedBeaconBlock + | typeof denebSsz.BlindedBeaconBlock + | typeof electraSsz.BlindedBeaconBlock >; SignedBeaconBlock: AllForksTypeOf< | typeof bellatrixSsz.SignedBlindedBeaconBlock | typeof capellaSsz.SignedBlindedBeaconBlock | typeof denebSsz.SignedBlindedBeaconBlock + | typeof electraSsz.SignedBlindedBeaconBlock >; }; @@ -259,40 +343,56 @@ export type AllForksLightClientSSZTypes = { | typeof bellatrixSsz.BeaconBlock | typeof capellaSsz.BeaconBlock | typeof denebSsz.BeaconBlock + | typeof electraSsz.BeaconBlock >; BeaconBlockBody: AllForksTypeOf< | typeof altairSsz.BeaconBlockBody | typeof bellatrixSsz.BeaconBlockBody | typeof capellaSsz.BeaconBlockBody | typeof denebSsz.BeaconBlockBody + | typeof electraSsz.BeaconBlockBody >; LightClientHeader: AllForksTypeOf< - typeof altairSsz.LightClientHeader | typeof capellaSsz.LightClientHeader | typeof denebSsz.LightClientHeader + | typeof altairSsz.LightClientHeader + | typeof capellaSsz.LightClientHeader + | typeof denebSsz.LightClientHeader + | typeof electraSsz.LightClientHeader >; LightClientBootstrap: AllForksTypeOf< | typeof altairSsz.LightClientBootstrap | typeof capellaSsz.LightClientBootstrap | typeof denebSsz.LightClientBootstrap + | typeof electraSsz.LightClientBootstrap >; LightClientUpdate: AllForksTypeOf< - typeof altairSsz.LightClientUpdate | typeof capellaSsz.LightClientUpdate | typeof denebSsz.LightClientUpdate + | typeof altairSsz.LightClientUpdate + | typeof capellaSsz.LightClientUpdate + | typeof denebSsz.LightClientUpdate + | typeof electraSsz.LightClientUpdate >; LightClientFinalityUpdate: AllForksTypeOf< | typeof altairSsz.LightClientFinalityUpdate | typeof capellaSsz.LightClientFinalityUpdate | typeof denebSsz.LightClientFinalityUpdate + | typeof electraSsz.LightClientFinalityUpdate >; LightClientOptimisticUpdate: AllForksTypeOf< | typeof altairSsz.LightClientOptimisticUpdate | typeof capellaSsz.LightClientOptimisticUpdate | typeof denebSsz.LightClientOptimisticUpdate + | typeof electraSsz.LightClientOptimisticUpdate >; LightClientStore: AllForksTypeOf< - typeof altairSsz.LightClientStore | typeof capellaSsz.LightClientStore | typeof denebSsz.LightClientStore + | typeof altairSsz.LightClientStore + | typeof capellaSsz.LightClientStore + | typeof denebSsz.LightClientStore + | typeof electraSsz.LightClientStore >; }; export type AllForksBlobsSSZTypes = { BlobSidecar: AllForksTypeOf; - ExecutionPayloadAndBlobsBundle: AllForksTypeOf; + ExecutionPayloadAndBlobsBundle: AllForksTypeOf< + typeof denebSsz.ExecutionPayloadAndBlobsBundle | typeof electraSsz.ExecutionPayloadAndBlobsBundle + >; }; diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index 30690a499845..b347b20df9c7 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -1,12 +1,37 @@ -import {ContainerType} from "@chainsafe/ssz"; +import {ContainerType, ListCompositeType, VectorCompositeType} from "@chainsafe/ssz"; +import { + HISTORICAL_ROOTS_LIMIT, + BLOCK_BODY_EXECUTION_PAYLOAD_DEPTH as EXECUTION_PAYLOAD_DEPTH, + EPOCHS_PER_SYNC_COMMITTEE_PERIOD, + SLOTS_PER_EPOCH, + MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD, +} from "@lodestar/params"; import {ssz as primitiveSsz} from "../primitive/index.js"; +import {ssz as phase0Ssz} from "../phase0/index.js"; +import {ssz as altairSsz} from "../altair/index.js"; +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 {BLSSignature} = primitiveSsz; +const {UintNum64, Slot, Root, BLSSignature, UintBn256, Bytes32, BLSPubkey, DepositIndex, UintBn64} = primitiveSsz; + +export const DepositReceipt = new ContainerType( + { + pubkey: BLSPubkey, + withdrawalCredentials: Bytes32, + amount: UintNum64, + signature: BLSSignature, + index: DepositIndex, + }, + {typeName: "DepositReceipt", jsonCase: "eth2"} +); + +export const DepositReceipts = new ListCompositeType(DepositReceipt, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD); export const ExecutionPayload = new ContainerType( { ...denebSsz.ExecutionPayload.fields, + depositReceipts: DepositReceipts, // New in ELECTRA }, {typeName: "ExecutionPayload", jsonCase: "eth2"} ); @@ -14,13 +39,18 @@ export const ExecutionPayload = new ContainerType( export const ExecutionPayloadHeader = new ContainerType( { ...denebSsz.ExecutionPayloadHeader.fields, + depositReceiptsRoot: Root, // New in ELECTRA }, {typeName: "ExecutionPayloadHeader", jsonCase: "eth2"} ); +// We have to preserve Fields ordering while changing the type of ExecutionPayload export const BeaconBlockBody = new ContainerType( { - ...denebSsz.BeaconBlockBody.fields, + ...altairSsz.BeaconBlockBody.fields, + executionPayload: ExecutionPayload, // Modified in ELECTRA + blsToExecutionChanges: capellaSsz.BeaconBlockBody.fields.blsToExecutionChanges, + blobKzgCommitments: denebSsz.BeaconBlockBody.fields.blobKzgCommitments, }, {typeName: "BeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} ); @@ -28,28 +58,25 @@ export const BeaconBlockBody = new ContainerType( export const BeaconBlock = new ContainerType( { ...denebSsz.BeaconBlock.fields, + body: BeaconBlockBody, // Modified in ELECTRA }, {typeName: "BeaconBlock", jsonCase: "eth2", cachePermanentRootStruct: true} ); export const SignedBeaconBlock = new ContainerType( { - message: BeaconBlock, + message: BeaconBlock, // Modified in ELECTRA signature: BLSSignature, }, {typeName: "SignedBeaconBlock", jsonCase: "eth2"} ); -export const BlobSidecar = new ContainerType( - { - ...denebSsz.BlobSidecar.fields, - }, - {typeName: "BlobSidecar", jsonCase: "eth2"} -); - export const BlindedBeaconBlockBody = new ContainerType( { - ...denebSsz.BlindedBeaconBlockBody.fields, + ...altairSsz.BeaconBlockBody.fields, + executionPayloadHeader: ExecutionPayloadHeader, // Modified in ELECTRA + blsToExecutionChanges: capellaSsz.BeaconBlockBody.fields.blsToExecutionChanges, + blobKzgCommitments: denebSsz.BeaconBlockBody.fields.blobKzgCommitments, }, {typeName: "BlindedBeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} ); @@ -57,13 +84,14 @@ export const BlindedBeaconBlockBody = new ContainerType( export const BlindedBeaconBlock = new ContainerType( { ...denebSsz.BlindedBeaconBlock.fields, + body: BlindedBeaconBlockBody, // Modified in ELECTRA }, {typeName: "BlindedBeaconBlock", jsonCase: "eth2", cachePermanentRootStruct: true} ); export const SignedBlindedBeaconBlock = new ContainerType( { - message: BlindedBeaconBlock, + message: BlindedBeaconBlock, // Modified in ELECTRA signature: BLSSignature, }, {typeName: "SignedBlindedBeaconBlock", jsonCase: "eth2"} @@ -71,7 +99,10 @@ export const SignedBlindedBeaconBlock = new ContainerType( export const BuilderBid = new ContainerType( { - ...denebSsz.BuilderBid.fields, + header: ExecutionPayloadHeader, // Modified in ELECTRA + blindedBlobsBundle: denebSsz.BlobKzgCommitments, + value: UintBn256, + pubkey: BLSPubkey, }, {typeName: "BuilderBid", jsonCase: "eth2"} ); @@ -86,63 +117,133 @@ export const SignedBuilderBid = new ContainerType( export const ExecutionPayloadAndBlobsBundle = new ContainerType( { - ...denebSsz.ExecutionPayloadAndBlobsBundle.fields, + executionPayload: ExecutionPayload, // Modified in ELECTRA + blobsBundle: denebSsz.BlobsBundle, }, {typeName: "ExecutionPayloadAndBlobsBundle", jsonCase: "eth2"} ); +// We don't spread deneb.BeaconState fields since we need to replace +// latestExecutionPayloadHeader and we cannot keep order doing that export const BeaconState = new ContainerType( { - ...denebSsz.BeaconState.fields, + genesisTime: UintNum64, + genesisValidatorsRoot: Root, + slot: primitiveSsz.Slot, + fork: phase0Ssz.Fork, + // History + latestBlockHeader: phase0Ssz.BeaconBlockHeader, + blockRoots: phase0Ssz.HistoricalBlockRoots, + stateRoots: phase0Ssz.HistoricalStateRoots, + // historical_roots Frozen in Capella, replaced by historical_summaries + historicalRoots: new ListCompositeType(Root, HISTORICAL_ROOTS_LIMIT), + // Eth1 + eth1Data: phase0Ssz.Eth1Data, + eth1DataVotes: phase0Ssz.Eth1DataVotes, + eth1DepositIndex: UintNum64, + // Registry + validators: phase0Ssz.Validators, + balances: phase0Ssz.Balances, + randaoMixes: phase0Ssz.RandaoMixes, + // Slashings + slashings: phase0Ssz.Slashings, + // Participation + previousEpochParticipation: altairSsz.EpochParticipation, + currentEpochParticipation: altairSsz.EpochParticipation, + // Finality + justificationBits: phase0Ssz.JustificationBits, + previousJustifiedCheckpoint: phase0Ssz.Checkpoint, + currentJustifiedCheckpoint: phase0Ssz.Checkpoint, + finalizedCheckpoint: phase0Ssz.Checkpoint, + // Inactivity + inactivityScores: altairSsz.InactivityScores, + // Sync + currentSyncCommittee: altairSsz.SyncCommittee, + nextSyncCommittee: altairSsz.SyncCommittee, + // Execution + latestExecutionPayloadHeader: ExecutionPayloadHeader, // Modified in ELECTRA + // Withdrawals + nextWithdrawalIndex: capellaSsz.BeaconState.fields.nextWithdrawalIndex, + nextWithdrawalValidatorIndex: capellaSsz.BeaconState.fields.nextWithdrawalValidatorIndex, + // Deep history valid from Capella onwards + historicalSummaries: capellaSsz.BeaconState.fields.historicalSummaries, + depositReceiptsStartIndex: UintBn64, // New in ELECTRA }, {typeName: "BeaconState", jsonCase: "eth2"} ); export const LightClientHeader = new ContainerType( { - ...denebSsz.LightClientHeader.fields, + beacon: phase0Ssz.BeaconBlockHeader, + execution: ExecutionPayloadHeader, // Modified in ELECTRA + executionBranch: new VectorCompositeType(Bytes32, EXECUTION_PAYLOAD_DEPTH), }, {typeName: "LightClientHeader", jsonCase: "eth2"} ); export const LightClientBootstrap = new ContainerType( { - ...denebSsz.LightClientBootstrap.fields, + header: LightClientHeader, + currentSyncCommittee: altairSsz.SyncCommittee, + currentSyncCommitteeBranch: altairSsz.LightClientBootstrap.fields.currentSyncCommitteeBranch, }, {typeName: "LightClientBootstrap", jsonCase: "eth2"} ); export const LightClientUpdate = new ContainerType( { - ...denebSsz.LightClientUpdate.fields, + attestedHeader: LightClientHeader, + nextSyncCommittee: altairSsz.SyncCommittee, + nextSyncCommitteeBranch: altairSsz.LightClientUpdate.fields.nextSyncCommitteeBranch, + finalizedHeader: LightClientHeader, + finalityBranch: altairSsz.LightClientUpdate.fields.finalityBranch, + syncAggregate: altairSsz.SyncAggregate, + signatureSlot: Slot, }, {typeName: "LightClientUpdate", jsonCase: "eth2"} ); export const LightClientFinalityUpdate = new ContainerType( { - ...denebSsz.LightClientFinalityUpdate.fields, + attestedHeader: LightClientHeader, + finalizedHeader: LightClientHeader, + finalityBranch: altairSsz.LightClientFinalityUpdate.fields.finalityBranch, + syncAggregate: altairSsz.SyncAggregate, + signatureSlot: Slot, }, {typeName: "LightClientFinalityUpdate", jsonCase: "eth2"} ); export const LightClientOptimisticUpdate = new ContainerType( { - ...denebSsz.LightClientOptimisticUpdate.fields, + attestedHeader: LightClientHeader, + syncAggregate: altairSsz.SyncAggregate, + signatureSlot: Slot, }, {typeName: "LightClientOptimisticUpdate", jsonCase: "eth2"} ); export const LightClientStore = new ContainerType( { - ...denebSsz.LightClientStore.fields, + snapshot: LightClientBootstrap, + validUpdates: new ListCompositeType(LightClientUpdate, EPOCHS_PER_SYNC_COMMITTEE_PERIOD * SLOTS_PER_EPOCH), }, {typeName: "LightClientStore", jsonCase: "eth2"} ); +// PayloadAttributes primarily for SSE event +export const PayloadAttributes = new ContainerType( + { + ...capellaSsz.PayloadAttributes.fields, + parentBeaconBlockRoot: Root, + }, + {typeName: "PayloadAttributes", jsonCase: "eth2"} +); + export const SSEPayloadAttributes = new ContainerType( { - ...denebSsz.SSEPayloadAttributes.fields, + ...bellatrixSsz.SSEPayloadAttributesCommon.fields, + payloadAttributes: PayloadAttributes, }, {typeName: "SSEPayloadAttributes", jsonCase: "eth2"} ); diff --git a/packages/types/src/electra/types.ts b/packages/types/src/electra/types.ts index 198259eed1dd..3286c10a0334 100644 --- a/packages/types/src/electra/types.ts +++ b/packages/types/src/electra/types.ts @@ -1,12 +1,14 @@ import {ValueOf} from "@chainsafe/ssz"; import * as ssz from "./sszTypes.js"; -export type BlobSidecar = ValueOf; -export type ExecutionPayloadAndBlobsBundle = ValueOf; +export type DepositReceipt = ValueOf; +export type DepositReceipts = ValueOf; export type ExecutionPayload = ValueOf; export type ExecutionPayloadHeader = ValueOf; +export type ExecutionPayloadAndBlobsBundle = ValueOf; + export type BeaconBlockBody = ValueOf; export type BeaconBlock = ValueOf; export type SignedBeaconBlock = ValueOf; @@ -17,6 +19,8 @@ export type BlindedBeaconBlockBody = ValueOf; export type BlindedBeaconBlock = ValueOf; export type SignedBlindedBeaconBlock = ValueOf; +export type FullOrBlindedExecutionPayload = ExecutionPayload | ExecutionPayloadHeader; + export type BuilderBid = ValueOf; export type SignedBuilderBid = ValueOf; export type SSEPayloadAttributes = ValueOf; diff --git a/packages/types/src/primitive/sszTypes.ts b/packages/types/src/primitive/sszTypes.ts index 068a32e2cc17..376e17c3f1b6 100644 --- a/packages/types/src/primitive/sszTypes.ts +++ b/packages/types/src/primitive/sszTypes.ts @@ -50,6 +50,7 @@ export const SubcommitteeIndex = UintNum64; */ export const ValidatorIndex = UintNum64; export const WithdrawalIndex = UintNum64; +export const DepositIndex = UintNum64; export const Gwei = UintBn64; export const Wei = UintBn256; export const Root = new ByteVectorType(32); diff --git a/packages/validator/src/util/params.ts b/packages/validator/src/util/params.ts index 0afede39b951..298707c07303 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -106,7 +106,6 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record Date: Fri, 12 Apr 2024 23:38:07 +0800 Subject: [PATCH 03/51] chore: fix CI failure due to recent merge from `unstable` (#6646) --- .../beacon-node/src/execution/engine/http.ts | 16 ++++++++-------- .../updateUnfinalizedPubkeys.test.ts | 6 +++--- .../test/sim/electra-interop.test.ts | 18 +++++++++--------- .../test/spec/utils/specTestIterator.ts | 1 + .../state-transition/src/cache/epochCache.ts | 2 +- 5 files changed, 22 insertions(+), 21 deletions(-) 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/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts b/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts index 900f6a6fb873..39bf1a1551c9 100644 --- a/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts +++ b/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts @@ -7,7 +7,7 @@ import bls from "@chainsafe/bls"; import {ssz} from "@lodestar/types"; import {type CachedBeaconStateAllForks, PubkeyIndexMap} from "@lodestar/state-transition"; import {bytesToBigInt, intToBytes} from "@lodestar/utils"; -import {CheckpointStateCache, StateContextCache} from "../../../../src/chain/stateCache/index.js"; +import {InMemoryCheckpointStateCache, StateContextCache} from "../../../../src/chain/stateCache/index.js"; import {generateCachedElectraState} from "../../../utils/state.js"; // Benchmark date from Mon Nov 21 2023 - Intel Core i7-9750H @ 2.60Ghz @@ -21,7 +21,7 @@ describe("updateUnfinalizedPubkeys perf tests", function () { const numCheckpointStateCache = 8; const numStateCache = 3 * 32; - let checkpointStateCache: CheckpointStateCache; + let checkpointStateCache: InMemoryCheckpointStateCache; let stateCache: StateContextCache; const unfinalizedPubkey2Index = generatePubkey2Index(0, Math.max.apply(null, numPubkeysToBeFinalizedCases)); @@ -35,7 +35,7 @@ describe("updateUnfinalizedPubkeys perf tests", function () { baseState.epochCtx.pubkey2index = new PubkeyIndexMap(); baseState.epochCtx.index2pubkey = []; - checkpointStateCache = new CheckpointStateCache({}); + checkpointStateCache = new InMemoryCheckpointStateCache({}); stateCache = new StateContextCache({}); for (let i = 0; i < numCheckpointStateCache; i++) { diff --git a/packages/beacon-node/test/sim/electra-interop.test.ts b/packages/beacon-node/test/sim/electra-interop.test.ts index c1663211cbd9..428bba3c63ad 100644 --- a/packages/beacon-node/test/sim/electra-interop.test.ts +++ b/packages/beacon-node/test/sim/electra-interop.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; +import assert from "node:assert"; import {describe, it, vi, afterAll, afterEach} from "vitest"; -/* eslint-disable @typescript-eslint/naming-convention */ -import _ from "lodash"; + import {LogLevel, sleep} from "@lodestar/utils"; import {ForkName, SLOTS_PER_EPOCH, UNSET_DEPOSIT_RECEIPTS_START_INDEX} from "@lodestar/params"; import {electra, Epoch, Slot} from "@lodestar/types"; @@ -225,13 +225,13 @@ describe("executionEngine / ExecutionEngineHttp", function () { } const actualDepositReceipt = payload.depositReceipts[0]; - if (!_.isEqual(actualDepositReceipt, depositReceiptB)) { - throw Error( - `Deposit receipts mismatched. Expected: ${JSON.stringify(depositReceiptB)}, actual: ${JSON.stringify( - actualDepositReceipt - )}` - ); - } + assert.deepStrictEqual( + actualDepositReceipt, + depositReceiptB, + `Deposit receipts mismatched. Expected: ${JSON.stringify(depositReceiptB)}, actual: ${JSON.stringify( + actualDepositReceipt + )}` + ); }); it("Post-merge, run for a few blocks", async function () { diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 3201fb15dbf3..6aa683cb6530 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -57,6 +57,7 @@ const coveredTestRunners = [ // ], // ``` export const defaultSkipOpts: SkipOpts = { + skippedForks: ["eip6110"], // TODO: capella // BeaconBlockBody proof in lightclient is the new addition in v1.3.0-rc.2-hotfix // Skip them for now to enable subsequently diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 97834c2afb2c..ade519719fc0 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -226,7 +226,7 @@ export class EpochCache { * eg. latest epoch = 105, latest finalized cp state epoch = 102 * then the list will be (in terms of epoch) [103, 104, 105] */ - private historicalValidatorLengths: immutable.List; + historicalValidatorLengths: immutable.List; constructor(data: { config: BeaconConfig; From ade5c5ddc1b7918b90f2dbd4a2d90cba6b594b3b Mon Sep 17 00:00:00 2001 From: g11tech Date: Mon, 22 Apr 2024 19:18:35 +0530 Subject: [PATCH 04/51] 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 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/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 71dfd63830c1..3c248ad4d194 100644 --- a/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts @@ -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; 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 d4a7f3a476e9..29bced11b236 100644 --- a/packages/beacon-node/test/unit/executionEngine/http.test.ts +++ b/packages/beacon-node/test/unit/executionEngine/http.test.ts @@ -189,6 +189,7 @@ describe("ExecutionEngine / http", () => { }, ], depositReceipts: null, // depositReceipts is null pre-electra + exits: null, }, null, // null returned for missing blocks { @@ -198,6 +199,7 @@ describe("ExecutionEngine / http", () => { ], withdrawals: null, // withdrawals is null pre-capella depositReceipts: null, // depositReceipts is null pre-electra + exits: null, }, ], }; @@ -246,6 +248,7 @@ describe("ExecutionEngine / http", () => { }, ], depositReceipts: null, // depositReceipts is null pre-electra + exits: null, }, null, // null returned for missing blocks { @@ -255,6 +258,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 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..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 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..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 Date: Wed, 1 May 2024 21:46:27 +0800 Subject: [PATCH 05/51] chore: update spec test version for electra fork (#6717) * Update spec-test version * Skip electra --- packages/beacon-node/test/spec/specTestVersioning.ts | 2 +- packages/beacon-node/test/spec/utils/specTestIterator.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/beacon-node/test/spec/specTestVersioning.ts b/packages/beacon-node/test/spec/specTestVersioning.ts index 37167c9bd5a1..06b02ab5304e 100644 --- a/packages/beacon-node/test/spec/specTestVersioning.ts +++ b/packages/beacon-node/test/spec/specTestVersioning.ts @@ -15,7 +15,7 @@ import {DownloadTestsOptions} from "@lodestar/spec-test-util/downloadTests"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export const ethereumConsensusSpecsTests: DownloadTestsOptions = { - specVersion: "v1.4.0-beta.6", + specVersion: "v1.5.0-alpha.1", // Target directory is the host package root: 'packages/*/spec-tests' outputDir: path.join(__dirname, "../../spec-tests"), specTestsRepoUrl: "https://github.com/ethereum/consensus-spec-tests", diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 6aa683cb6530..88d7cbdea9e6 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -57,7 +57,7 @@ const coveredTestRunners = [ // ], // ``` export const defaultSkipOpts: SkipOpts = { - skippedForks: ["eip6110"], + skippedForks: ["electra", "eip7594"], // TODO: capella // BeaconBlockBody proof in lightclient is the new addition in v1.3.0-rc.2-hotfix // Skip them for now to enable subsequently @@ -65,7 +65,7 @@ export const defaultSkipOpts: SkipOpts = { "capella/light_client/single_merkle_proof/BeaconBlockBody", "deneb/light_client/single_merkle_proof/BeaconBlockBody", ], - skippedRunners: ["merkle_proof"], + skippedRunners: ["merkle_proof", "networking"], }; /** From ea52e9d38d144f6ae34ac7e23d98108a61dbe84b Mon Sep 17 00:00:00 2001 From: NC Date: Sat, 4 May 2024 21:24:26 +0800 Subject: [PATCH 06/51] feat: add presets and ssz types for EIP-7549 (#6715) * Add types * Update unit test * lint * Address comments * Address comments * Lint * Update packages/beacon-node/src/util/sszBytes.ts Co-authored-by: tuyennhv * Add isElectraAttestation * Update unit test * Update unit test * chore: add comments for sszBytes.ts --------- Co-authored-by: tuyennhv Co-authored-by: Tuyen Nguyen --- .../src/chain/validation/attestation.ts | 4 +- packages/beacon-node/src/util/sszBytes.ts | 63 +++++++--- .../test/unit/util/sszBytes.test.ts | 46 +++++-- packages/params/src/index.ts | 2 + packages/params/src/presets/mainnet.ts | 2 + packages/params/src/presets/minimal.ts | 2 + packages/params/src/types.ts | 4 + packages/types/src/allForks/sszTypes.ts | 18 +++ packages/types/src/allForks/types.ts | 6 + packages/types/src/electra/sszTypes.ts | 113 +++++++++++++++++- packages/types/src/electra/types.ts | 8 ++ packages/types/src/utils/typeguards.ts | 5 + packages/validator/src/util/params.ts | 2 + 13 files changed, 245 insertions(+), 30 deletions(-) diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index fc39534b45e6..7b80d75e3063 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -305,7 +305,7 @@ async function validateGossipAttestationNoSignatureCheck( // > TODO: Do this check **before** getting the target state but don't recompute zipIndexes const aggregationBits = attestationOrCache.attestation ? attestationOrCache.attestation.aggregationBits - : getAggregationBitsFromAttestationSerialized(attestationOrCache.serializedData); + : getAggregationBitsFromAttestationSerialized(fork, attestationOrCache.serializedData); if (aggregationBits === null) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.INVALID_SERIALIZED_BYTES, @@ -414,7 +414,7 @@ async function validateGossipAttestationNoSignatureCheck( let attDataRootHex: RootHex; const signature = attestationOrCache.attestation ? attestationOrCache.attestation.signature - : getSignatureFromAttestationSerialized(attestationOrCache.serializedData); + : getSignatureFromAttestationSerialized(fork, attestationOrCache.serializedData); if (signature === null) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.INVALID_SERIALIZED_BYTES, diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index 802b9a266ab1..70b2dddfaddf 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -1,16 +1,31 @@ import {BitArray, deserializeUint8ArrayBitListFromBytes} from "@chainsafe/ssz"; import {BLSSignature, RootHex, Slot} from "@lodestar/types"; import {toHex} from "@lodestar/utils"; -import {BYTES_PER_FIELD_ELEMENT, FIELD_ELEMENTS_PER_BLOB} from "@lodestar/params"; +import { + BYTES_PER_FIELD_ELEMENT, + FIELD_ELEMENTS_PER_BLOB, + ForkName, + ForkSeq, + MAX_COMMITTEES_PER_SLOT, +} from "@lodestar/params"; export type BlockRootHex = RootHex; export type AttDataBase64 = string; +// pre-electra // class Attestation(Container): // aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE] - offset 4 // data: AttestationData - target data - 128 // signature: BLSSignature - 96 + +// electra +// class Attestation(Container): +// aggregation_bits: BitList[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] - offset 4 +// data: AttestationData - target data - 128 +// committee_bits: BitVector[MAX_COMMITTEES_PER_SLOT] +// signature: BLSSignature - 96 // +// for all forks // class AttestationData(Container): 128 bytes fixed size // slot: Slot - data 8 // index: CommitteeIndex - data 8 @@ -23,6 +38,7 @@ const ATTESTATION_BEACON_BLOCK_ROOT_OFFSET = VARIABLE_FIELD_OFFSET + 8 + 8; const ROOT_SIZE = 32; const SLOT_SIZE = 8; const ATTESTATION_DATA_SIZE = 128; +const COMMITTEE_BITS_SIZE = Math.max(Math.ceil(MAX_COMMITTEES_PER_SLOT / 8), 1); const SIGNATURE_SIZE = 96; /** @@ -66,16 +82,17 @@ export function getAttDataBase64FromAttestationSerialized(data: Uint8Array): Att * Extract aggregation bits from attestation serialized bytes. * Return null if data is not long enough to extract aggregation bits. */ -export function getAggregationBitsFromAttestationSerialized(data: Uint8Array): BitArray | null { - if (data.length < VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE) { +export function getAggregationBitsFromAttestationSerialized(fork: ForkName, data: Uint8Array): BitArray | null { + const aggregationBitsStartIndex = + ForkSeq[fork] >= ForkSeq.electra + ? VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + COMMITTEE_BITS_SIZE + SIGNATURE_SIZE + : VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE; + + if (data.length < aggregationBitsStartIndex) { return null; } - const {uint8Array, bitLen} = deserializeUint8ArrayBitListFromBytes( - data, - VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE, - data.length - ); + const {uint8Array, bitLen} = deserializeUint8ArrayBitListFromBytes(data, aggregationBitsStartIndex, data.length); return new BitArray(uint8Array, bitLen); } @@ -83,15 +100,33 @@ export function getAggregationBitsFromAttestationSerialized(data: Uint8Array): B * Extract signature from attestation serialized bytes. * Return null if data is not long enough to extract signature. */ -export function getSignatureFromAttestationSerialized(data: Uint8Array): BLSSignature | null { - if (data.length < VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE) { +export function getSignatureFromAttestationSerialized(fork: ForkName, data: Uint8Array): BLSSignature | null { + const signatureStartIndex = + ForkSeq[fork] >= ForkSeq.electra + ? VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + COMMITTEE_BITS_SIZE + : VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE; + + if (data.length < signatureStartIndex + SIGNATURE_SIZE) { return null; } - return data.subarray( - VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE, - VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE - ); + return data.subarray(signatureStartIndex, signatureStartIndex + SIGNATURE_SIZE); +} + +/** + * Extract committee bits from Electra attestation serialized bytes. + * Return null if data is not long enough to extract committee bits. + */ +export function getCommitteeBitsFromAttestationSerialized(data: Uint8Array): BitArray | null { + const committeeBitsStartIndex = VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE; + + if (data.length < committeeBitsStartIndex + COMMITTEE_BITS_SIZE) { + return null; + } + + const uint8Array = data.subarray(committeeBitsStartIndex, committeeBitsStartIndex + COMMITTEE_BITS_SIZE); + + return new BitArray(uint8Array, MAX_COMMITTEES_PER_SLOT); } // diff --git a/packages/beacon-node/test/unit/util/sszBytes.test.ts b/packages/beacon-node/test/unit/util/sszBytes.test.ts index bb5fc67a7ce6..d0dc150cd792 100644 --- a/packages/beacon-node/test/unit/util/sszBytes.test.ts +++ b/packages/beacon-node/test/unit/util/sszBytes.test.ts @@ -1,6 +1,8 @@ import {describe, it, expect} from "vitest"; -import {deneb, Epoch, phase0, RootHex, Slot, ssz} from "@lodestar/types"; +import {BitArray} from "@chainsafe/ssz"; +import {allForks, deneb, Epoch, isElectraAttestation, phase0, RootHex, Slot, ssz} from "@lodestar/types"; import {fromHex, toHex} from "@lodestar/utils"; +import {ForkName, MAX_COMMITTEES_PER_SLOT} from "@lodestar/params"; import { getAttDataBase64FromAttestationSerialized, getAttDataBase64FromSignedAggregateAndProofSerialized, @@ -12,10 +14,11 @@ import { getSignatureFromAttestationSerialized, getSlotFromSignedBeaconBlockSerialized, getSlotFromBlobSidecarSerialized, + getCommitteeBitsFromAttestationSerialized, } from "../../../src/util/sszBytes.js"; describe("attestation SSZ serialized picking", () => { - const testCases: phase0.Attestation[] = [ + const testCases: allForks.Attestation[] = [ ssz.phase0.Attestation.defaultValue(), attestationFromValues( 4_000_000, @@ -23,18 +26,40 @@ describe("attestation SSZ serialized picking", () => { 200_00, "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffffffffff" ), + ssz.electra.Attestation.defaultValue(), + { + ...attestationFromValues( + 4_000_000, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + 200_00, + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffffffffff" + ), + committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, 3), + }, ]; for (const [i, attestation] of testCases.entries()) { it(`attestation ${i}`, () => { - const bytes = ssz.phase0.Attestation.serialize(attestation); + const isElectra = isElectraAttestation(attestation); + const bytes = isElectra + ? ssz.electra.Attestation.serialize(attestation) + : ssz.phase0.Attestation.serialize(attestation); expect(getSlotFromAttestationSerialized(bytes)).toBe(attestation.data.slot); expect(getBlockRootFromAttestationSerialized(bytes)).toBe(toHex(attestation.data.beaconBlockRoot)); - expect(getAggregationBitsFromAttestationSerialized(bytes)?.toBoolArray()).toEqual( - attestation.aggregationBits.toBoolArray() - ); - expect(getSignatureFromAttestationSerialized(bytes)).toEqual(attestation.signature); + + if (isElectra) { + expect(getAggregationBitsFromAttestationSerialized(ForkName.electra, bytes)?.toBoolArray()).toEqual( + attestation.aggregationBits.toBoolArray() + ); + expect(getCommitteeBitsFromAttestationSerialized(bytes)).toEqual(attestation.committeeBits); + expect(getSignatureFromAttestationSerialized(ForkName.electra, bytes)).toEqual(attestation.signature); + } else { + expect(getAggregationBitsFromAttestationSerialized(ForkName.phase0, bytes)?.toBoolArray()).toEqual( + attestation.aggregationBits.toBoolArray() + ); + expect(getSignatureFromAttestationSerialized(ForkName.phase0, bytes)).toEqual(attestation.signature); + } const attDataBase64 = ssz.phase0.AttestationData.serialize(attestation.data); expect(getAttDataBase64FromAttestationSerialized(bytes)).toBe(Buffer.from(attDataBase64).toString("base64")); @@ -65,14 +90,16 @@ describe("attestation SSZ serialized picking", () => { it("getAggregateionBitsFromAttestationSerialized - invalid data", () => { const invalidAggregationBitsDataSizes = [0, 4, 100, 128, 227]; for (const size of invalidAggregationBitsDataSizes) { - expect(getAggregationBitsFromAttestationSerialized(Buffer.alloc(size))).toBeNull(); + expect(getAggregationBitsFromAttestationSerialized(ForkName.phase0, Buffer.alloc(size))).toBeNull(); + expect(getAggregationBitsFromAttestationSerialized(ForkName.electra, Buffer.alloc(size))).toBeNull(); } }); it("getSignatureFromAttestationSerialized - invalid data", () => { const invalidSignatureDataSizes = [0, 4, 100, 128, 227]; for (const size of invalidSignatureDataSizes) { - expect(getSignatureFromAttestationSerialized(Buffer.alloc(size))).toBeNull(); + expect(getSignatureFromAttestationSerialized(ForkName.phase0, Buffer.alloc(size))).toBeNull(); + expect(getSignatureFromAttestationSerialized(ForkName.electra, Buffer.alloc(size))).toBeNull(); } }); }); @@ -86,6 +113,7 @@ describe("aggregateAndProof SSZ serialized picking", () => { 200_00, "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffffffffff" ), + ssz.electra.SignedAggregateAndProof.defaultValue(), ]; for (const [i, signedAggregateAndProof] of testCases.entries()) { diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index d903a404dfb5..3e56effc4138 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -96,6 +96,8 @@ export const { MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD, MAX_EXECUTION_LAYER_EXITS, + MAX_ATTESTER_SLASHINGS_ELECTRA, + MAX_ATTESTATIONS_ELECTRA, } = activePreset; //////////// diff --git a/packages/params/src/presets/mainnet.ts b/packages/params/src/presets/mainnet.ts index 802a6691c311..27cb7640b2dd 100644 --- a/packages/params/src/presets/mainnet.ts +++ b/packages/params/src/presets/mainnet.ts @@ -122,4 +122,6 @@ export const mainnetPreset: BeaconPreset = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 8192, MAX_EXECUTION_LAYER_EXITS: 16, + MAX_ATTESTER_SLASHINGS_ELECTRA: 1, + MAX_ATTESTATIONS_ELECTRA: 8, }; diff --git a/packages/params/src/presets/minimal.ts b/packages/params/src/presets/minimal.ts index d10b420ed97c..022532a49e6f 100644 --- a/packages/params/src/presets/minimal.ts +++ b/packages/params/src/presets/minimal.ts @@ -123,4 +123,6 @@ export const minimalPreset: BeaconPreset = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 4, MAX_EXECUTION_LAYER_EXITS: 16, + MAX_ATTESTER_SLASHINGS_ELECTRA: 1, + MAX_ATTESTATIONS_ELECTRA: 8, }; diff --git a/packages/params/src/types.ts b/packages/params/src/types.ts index 7856f1be72ba..34f40a66707e 100644 --- a/packages/params/src/types.ts +++ b/packages/params/src/types.ts @@ -86,6 +86,8 @@ export type BeaconPreset = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: number; MAX_EXECUTION_LAYER_EXITS: number; + MAX_ATTESTER_SLASHINGS_ELECTRA: number; + MAX_ATTESTATIONS_ELECTRA: number; }; /** @@ -175,6 +177,8 @@ export const beaconPresetTypes: BeaconPresetTypes = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: "number", MAX_EXECUTION_LAYER_EXITS: "number", + MAX_ATTESTER_SLASHINGS_ELECTRA: "number", + MAX_ATTESTATIONS_ELECTRA: "number", }; type BeaconPresetTypes = { diff --git a/packages/types/src/allForks/sszTypes.ts b/packages/types/src/allForks/sszTypes.ts index 84c6bb86ce5f..0ad3b18b2f8e 100644 --- a/packages/types/src/allForks/sszTypes.ts +++ b/packages/types/src/allForks/sszTypes.ts @@ -16,6 +16,9 @@ export const allForks = { SignedBeaconBlock: phase0.SignedBeaconBlock, BeaconState: phase0.BeaconState, Metadata: phase0.Metadata, + SignedAggregateAndProof: phase0.SignedAggregateAndProof, + Attestation: phase0.Attestation, + AttesterSlashing: phase0.AttesterSlashing, }, altair: { BeaconBlockBody: altair.BeaconBlockBody, @@ -23,6 +26,9 @@ export const allForks = { SignedBeaconBlock: altair.SignedBeaconBlock, BeaconState: altair.BeaconState, Metadata: altair.Metadata, + SignedAggregateAndProof: phase0.SignedAggregateAndProof, + Attestation: phase0.Attestation, + AttesterSlashing: phase0.AttesterSlashing, }, bellatrix: { BeaconBlockBody: bellatrix.BeaconBlockBody, @@ -30,6 +36,9 @@ export const allForks = { SignedBeaconBlock: bellatrix.SignedBeaconBlock, BeaconState: bellatrix.BeaconState, Metadata: altair.Metadata, + SignedAggregateAndProof: phase0.SignedAggregateAndProof, + Attestation: phase0.Attestation, + AttesterSlashing: phase0.AttesterSlashing, }, capella: { BeaconBlockBody: capella.BeaconBlockBody, @@ -37,6 +46,9 @@ export const allForks = { SignedBeaconBlock: capella.SignedBeaconBlock, BeaconState: capella.BeaconState, Metadata: altair.Metadata, + SignedAggregateAndProof: phase0.SignedAggregateAndProof, + Attestation: phase0.Attestation, + AttesterSlashing: phase0.AttesterSlashing, }, deneb: { BeaconBlockBody: deneb.BeaconBlockBody, @@ -44,6 +56,9 @@ export const allForks = { SignedBeaconBlock: deneb.SignedBeaconBlock, BeaconState: deneb.BeaconState, Metadata: altair.Metadata, + SignedAggregateAndProof: phase0.SignedAggregateAndProof, + Attestation: phase0.Attestation, + AttesterSlashing: phase0.AttesterSlashing, }, electra: { BeaconBlockBody: electra.BeaconBlockBody, @@ -51,6 +66,9 @@ export const allForks = { SignedBeaconBlock: electra.SignedBeaconBlock, BeaconState: electra.BeaconState, Metadata: altair.Metadata, + SignedAggregateAndProof: electra.SignedAggregateAndProof, + Attestation: electra.Attestation, + AttesterSlashing: electra.AttesterSlashing, }, }; diff --git a/packages/types/src/allForks/types.ts b/packages/types/src/allForks/types.ts index 416bb051065a..f116bde20b70 100644 --- a/packages/types/src/allForks/types.ts +++ b/packages/types/src/allForks/types.ts @@ -43,6 +43,12 @@ export type BeaconState = | capella.BeaconState | deneb.BeaconState | electra.BeaconState; +export type Attestation = phase0.Attestation | electra.Attestation; +export type AggregateAndProof = phase0.AggregateAndProof | electra.AggregateAndProof; +export type SignedAggregateAndProof = phase0.SignedAggregateAndProof | electra.SignedAggregateAndProof; +export type IndexedAttestation = phase0.IndexedAttestation | electra.IndexedAttestation; +export type IndexedAttestationBigint = phase0.IndexedAttestationBigint | electra.IndexedAttestationBigint; +export type AttesterSlashing = phase0.AttesterSlashing | electra.AttesterSlashing; export type Metadata = phase0.Metadata | altair.Metadata; // For easy reference in the assemble block for building payloads diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index 7b5ed51fb786..3404baf04110 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -1,10 +1,21 @@ -import {ContainerType, ListCompositeType, VectorCompositeType} from "@chainsafe/ssz"; +import { + BitListType, + BitVectorType, + ContainerType, + ListBasicType, + ListCompositeType, + VectorCompositeType, +} from "@chainsafe/ssz"; import { HISTORICAL_ROOTS_LIMIT, BLOCK_BODY_EXECUTION_PAYLOAD_DEPTH as EXECUTION_PAYLOAD_DEPTH, EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD, + MAX_VALIDATORS_PER_COMMITTEE, + MAX_COMMITTEES_PER_SLOT, + MAX_ATTESTATIONS_ELECTRA, + MAX_ATTESTER_SLASHINGS_ELECTRA, MAX_EXECUTION_LAYER_EXITS, } from "@lodestar/params"; import {ssz as primitiveSsz} from "../primitive/index.js"; @@ -14,8 +25,84 @@ 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, ExecutionAddress} = - primitiveSsz; +const { + UintNum64, + Slot, + Root, + BLSSignature, + UintBn256, + Bytes32, + BLSPubkey, + DepositIndex, + UintBn64, + ExecutionAddress, + ValidatorIndex, +} = primitiveSsz; + +export const AggregationBits = new BitListType(MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT); + +// This CommitteeBits serves a different purpose than CommitteeBits in phase0 +// TODO Electra: Rename phase0.CommitteeBits to ParticipationBits to avoid confusion +export const CommitteeBits = new BitVectorType(MAX_COMMITTEES_PER_SLOT); + +export const AttestingIndices = new ListBasicType( + ValidatorIndex, + MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT +); + +export const Attestation = new ContainerType( + { + aggregationBits: AggregationBits, // Modified in ELECTRA + data: phase0Ssz.AttestationData, + committeeBits: CommitteeBits, // New in ELECTRA + signature: BLSSignature, + }, + {typeName: "Attestation", jsonCase: "eth2"} +); + +export const IndexedAttestation = new ContainerType( + { + attestingIndices: AttestingIndices, // Modified in ELECTRA + data: phase0Ssz.AttestationData, + signature: BLSSignature, + }, + {typeName: "IndexedAttestation", jsonCase: "eth2"} +); + +/** Same as `IndexedAttestation` but epoch, slot and index are not bounded and must be a bigint */ +export const IndexedAttestationBigint = new ContainerType( + { + attestingIndices: AttestingIndices, // Modified in ELECTRA + data: phase0Ssz.AttestationDataBigint, + signature: BLSSignature, + }, + {typeName: "IndexedAttestation", jsonCase: "eth2"} +); + +export const AttesterSlashing = new ContainerType( + { + attestation1: IndexedAttestationBigint, // Modified in ELECTRA + attestation2: IndexedAttestationBigint, // Modified in ELECTRA + }, + {typeName: "AttesterSlashing", jsonCase: "eth2"} +); + +export const AggregateAndProof = new ContainerType( + { + aggregatorIndex: ValidatorIndex, + aggregate: Attestation, // Modified in ELECTRA + selectionProof: BLSSignature, + }, + {typeName: "AggregateAndProof", jsonCase: "eth2", cachePermanentRootStruct: true} +); + +export const SignedAggregateAndProof = new ContainerType( + { + message: AggregateAndProof, // Modified in ELECTRA + signature: BLSSignature, + }, + {typeName: "SignedAggregateAndProof", jsonCase: "eth2"} +); export const DepositReceipt = new ContainerType( { @@ -60,7 +147,15 @@ export const ExecutionPayloadHeader = new ContainerType( // We have to preserve Fields ordering while changing the type of ExecutionPayload export const BeaconBlockBody = new ContainerType( { - ...altairSsz.BeaconBlockBody.fields, + randaoReveal: phase0Ssz.BeaconBlockBody.fields.randaoReveal, + eth1Data: phase0Ssz.BeaconBlockBody.fields.eth1Data, + graffiti: phase0Ssz.BeaconBlockBody.fields.graffiti, + proposerSlashings: phase0Ssz.BeaconBlockBody.fields.proposerSlashings, + attesterSlashings: new ListCompositeType(AttesterSlashing, MAX_ATTESTER_SLASHINGS_ELECTRA), // Modified in ELECTRA + attestations: new ListCompositeType(Attestation, MAX_ATTESTATIONS_ELECTRA), // Modified in ELECTRA + deposits: phase0Ssz.BeaconBlockBody.fields.deposits, + voluntaryExits: phase0Ssz.BeaconBlockBody.fields.voluntaryExits, + syncAggregate: altairSsz.BeaconBlockBody.fields.syncAggregate, executionPayload: ExecutionPayload, // Modified in ELECTRA blsToExecutionChanges: capellaSsz.BeaconBlockBody.fields.blsToExecutionChanges, blobKzgCommitments: denebSsz.BeaconBlockBody.fields.blobKzgCommitments, @@ -86,7 +181,15 @@ export const SignedBeaconBlock = new ContainerType( export const BlindedBeaconBlockBody = new ContainerType( { - ...altairSsz.BeaconBlockBody.fields, + randaoReveal: phase0Ssz.BeaconBlockBody.fields.randaoReveal, + eth1Data: phase0Ssz.BeaconBlockBody.fields.eth1Data, + graffiti: phase0Ssz.BeaconBlockBody.fields.graffiti, + proposerSlashings: phase0Ssz.BeaconBlockBody.fields.proposerSlashings, + attesterSlashings: new ListCompositeType(AttesterSlashing, MAX_ATTESTER_SLASHINGS_ELECTRA), // Modified in ELECTRA + attestations: new ListCompositeType(Attestation, MAX_ATTESTATIONS_ELECTRA), // Modified in ELECTRA + deposits: phase0Ssz.BeaconBlockBody.fields.deposits, + voluntaryExits: phase0Ssz.BeaconBlockBody.fields.voluntaryExits, + syncAggregate: altairSsz.SyncAggregate, executionPayloadHeader: ExecutionPayloadHeader, // Modified in ELECTRA blsToExecutionChanges: capellaSsz.BeaconBlockBody.fields.blsToExecutionChanges, blobKzgCommitments: denebSsz.BeaconBlockBody.fields.blobKzgCommitments, diff --git a/packages/types/src/electra/types.ts b/packages/types/src/electra/types.ts index 1b9b42217b8c..2925885b3af3 100644 --- a/packages/types/src/electra/types.ts +++ b/packages/types/src/electra/types.ts @@ -1,6 +1,14 @@ import {ValueOf} from "@chainsafe/ssz"; import * as ssz from "./sszTypes.js"; +export type Attestation = ValueOf; +export type IndexedAttestation = ValueOf; +export type IndexedAttestationBigint = ValueOf; +export type AttesterSlashing = ValueOf; + +export type AggregateAndProof = ValueOf; +export type SignedAggregateAndProof = ValueOf; + export type DepositReceipt = ValueOf; export type DepositReceipts = ValueOf; diff --git a/packages/types/src/utils/typeguards.ts b/packages/types/src/utils/typeguards.ts index 781738c3dbad..22c75302c159 100644 --- a/packages/types/src/utils/typeguards.ts +++ b/packages/types/src/utils/typeguards.ts @@ -14,6 +14,7 @@ import { ExecutionPayload, ExecutionPayloadAndBlobsBundle, } from "../allForks/types.js"; +import {allForks, electra} from "../types.js"; export function isBlindedExecution(payload: FullOrBlindedExecutionPayload): payload is ExecutionPayloadHeader { // we just check transactionsRoot for determinging as it the base field @@ -49,3 +50,7 @@ export function isExecutionPayloadAndBlobsBundle( ): data is ExecutionPayloadAndBlobsBundle { return (data as ExecutionPayloadAndBlobsBundle).blobsBundle !== undefined; } + +export function isElectraAttestation(attestation: allForks.Attestation): attestation is electra.Attestation { + return (attestation as electra.Attestation).committeeBits !== undefined; +} diff --git a/packages/validator/src/util/params.ts b/packages/validator/src/util/params.ts index ca1b36883a90..1eda005b70c3 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -225,5 +225,7 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record Date: Sun, 5 May 2024 00:17:57 +0530 Subject: [PATCH 07/51] chore: fix the rebase build (#6735) * chore: fix the rebase build * fix test --- packages/beacon-node/src/chain/regen/queued.ts | 2 +- .../perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/beacon-node/src/chain/regen/queued.ts b/packages/beacon-node/src/chain/regen/queued.ts index b988d98b8c55..5a049134e91f 100644 --- a/packages/beacon-node/src/chain/regen/queued.ts +++ b/packages/beacon-node/src/chain/regen/queued.ts @@ -201,7 +201,7 @@ export class QueuedStateRegenerator implements IStateRegenerator { */ updateUnfinalizedPubkeys(validators: UnfinalizedPubkeyIndexMap): void { let numStatesUpdated = 0; - const states = this.stateCache.getStates(); + const states = this.blockStateCache.getStates(); const cpStates = this.checkpointStateCache.getStates(); // Add finalized pubkeys to all states. diff --git a/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts b/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts index 39bf1a1551c9..b8f5c30a70ea 100644 --- a/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts +++ b/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts @@ -7,7 +7,8 @@ import bls from "@chainsafe/bls"; import {ssz} from "@lodestar/types"; import {type CachedBeaconStateAllForks, PubkeyIndexMap} from "@lodestar/state-transition"; import {bytesToBigInt, intToBytes} from "@lodestar/utils"; -import {InMemoryCheckpointStateCache, StateContextCache} from "../../../../src/chain/stateCache/index.js"; +import {InMemoryCheckpointStateCache, BlockStateCacheImpl} from "../../../../src/chain/stateCache/index.js"; +import {BlockStateCache} from "../../../../src/chain/stateCache/types.js"; import {generateCachedElectraState} from "../../../utils/state.js"; // Benchmark date from Mon Nov 21 2023 - Intel Core i7-9750H @ 2.60Ghz @@ -22,7 +23,7 @@ describe("updateUnfinalizedPubkeys perf tests", function () { const numStateCache = 3 * 32; let checkpointStateCache: InMemoryCheckpointStateCache; - let stateCache: StateContextCache; + let stateCache: BlockStateCache; const unfinalizedPubkey2Index = generatePubkey2Index(0, Math.max.apply(null, numPubkeysToBeFinalizedCases)); const baseState = generateCachedElectraState(); @@ -36,7 +37,7 @@ describe("updateUnfinalizedPubkeys perf tests", function () { baseState.epochCtx.index2pubkey = []; checkpointStateCache = new InMemoryCheckpointStateCache({}); - stateCache = new StateContextCache({}); + stateCache = new BlockStateCacheImpl({}); for (let i = 0; i < numCheckpointStateCache; i++) { const clonedState = baseState.clone(); From 2b8866d7fcdb4543fe8e74fcfddfea3ad49dd012 Mon Sep 17 00:00:00 2001 From: g11tech Date: Mon, 6 May 2024 00:08:09 +0530 Subject: [PATCH 08/51] feat: upgrade 7002 exits to withdrawal request (#6736) * feat: upgrade 7002 exits to withdrawal request * fix types * fix types and references * further fix the types references and get build passing * update the process ops fn but needs to be extended by maxeb --- .../src/execution/engine/payloadIdCache.ts | 3 +- .../beacon-node/src/execution/engine/types.ts | 50 ++++++++++++------- packages/light-client/src/spec/utils.ts | 6 +-- packages/params/src/index.ts | 2 +- packages/params/src/presets/mainnet.ts | 2 +- packages/params/src/presets/minimal.ts | 2 +- packages/params/src/types.ts | 4 +- ...processExecutionLayerWithdrawalRequest.ts} | 25 +++++++--- .../src/block/processOperations.ts | 8 +-- .../src/slot/upgradeStateToElectra.ts | 2 +- .../state-transition/src/util/execution.ts | 7 +-- packages/types/src/electra/sszTypes.ts | 16 +++--- packages/types/src/electra/types.ts | 4 +- packages/validator/src/util/params.ts | 2 +- 14 files changed, 81 insertions(+), 52 deletions(-) rename packages/state-transition/src/block/{processExecutionLayerExit.ts => processExecutionLayerWithdrawalRequest.ts} (71%) diff --git a/packages/beacon-node/src/execution/engine/payloadIdCache.ts b/packages/beacon-node/src/execution/engine/payloadIdCache.ts index 960b061f12da..b5fe3d33e267 100644 --- a/packages/beacon-node/src/execution/engine/payloadIdCache.ts +++ b/packages/beacon-node/src/execution/engine/payloadIdCache.ts @@ -26,9 +26,10 @@ export type DepositReceiptV1 = { index: QUANTITY; }; -export type ExecutionLayerExitV1 = { +export type ExecutionLayerWithdrawalRequestV1 = { sourceAddress: DATA; validatorPubkey: DATA; + amount: QUANTITY; }; type FcuAttributes = {headBlockHash: DATA; finalizedBlockHash: DATA} & Omit; diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index c3da8e37826e..b72b00bc2c4d 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, ExecutionLayerExitV1} from "./payloadIdCache.js"; +import {WithdrawalV1, DepositReceiptV1, ExecutionLayerWithdrawalRequestV1} from "./payloadIdCache.js"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -119,14 +119,14 @@ export type ExecutionPayloadBodyRpc = { transactions: DATA[]; withdrawals: WithdrawalV1[] | null | undefined; depositReceipts: DepositReceiptV1[] | null | undefined; - exits: ExecutionLayerExitV1[] | null | undefined; + withdrawalRequests: ExecutionLayerWithdrawalRequestV1[] | null | undefined; }; export type ExecutionPayloadBody = { transactions: bellatrix.Transaction[]; withdrawals: capella.Withdrawals | null; depositReceipts: electra.DepositReceipts | null; - exits: electra.ExecutionLayerExits | null; + withdrawalRequests: electra.ExecutionLayerWithdrawalRequests | null; }; export type ExecutionPayloadRpc = { @@ -149,7 +149,7 @@ export type ExecutionPayloadRpc = { excessBlobGas?: QUANTITY; // DENEB parentBeaconBlockRoot?: QUANTITY; // DENEB depositReceipts?: DepositReceiptRpc[]; // ELECTRA - exits?: ExecutionLayerExitRpc[]; // ELECTRA + withdrawalRequests?: ExecutionLayerWithdrawalRequestRpc[]; // ELECTRA }; export type WithdrawalRpc = { @@ -160,7 +160,7 @@ export type WithdrawalRpc = { }; export type DepositReceiptRpc = DepositReceiptV1; -export type ExecutionLayerExitRpc = ExecutionLayerExitV1; +export type ExecutionLayerWithdrawalRequestRpc = ExecutionLayerWithdrawalRequestV1; export type VersionedHashesRpc = DATA[]; @@ -215,9 +215,9 @@ export function serializeExecutionPayload(fork: ForkName, data: allForks.Executi // ELECTRA adds depositReceipts to the ExecutionPayload if (ForkSeq[fork] >= ForkSeq.electra) { - const {depositReceipts, exits} = data as electra.ExecutionPayload; + const {depositReceipts, withdrawalRequests} = data as electra.ExecutionPayload; payload.depositReceipts = depositReceipts.map(serializeDepositReceipt); - payload.exits = exits.map(serializeExecutionLayerExit); + payload.withdrawalRequests = withdrawalRequests.map(serializeExecutionLayerWithdrawalRequest); } return payload; @@ -306,7 +306,7 @@ export function parseExecutionPayload( } if (ForkSeq[fork] >= ForkSeq.electra) { - const {depositReceipts, exits} = data; + const {depositReceipts, withdrawalRequests} = data; // Geth can also reply with null if (depositReceipts == null) { throw Error( @@ -315,12 +315,14 @@ export function parseExecutionPayload( } (executionPayload as electra.ExecutionPayload).depositReceipts = depositReceipts.map(deserializeDepositReceipt); - if (exits == null) { + if (withdrawalRequests == null) { throw Error( - `exits missing for ${fork} >= electra executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}` + `withdrawalRequests missing for ${fork} >= electra executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}` ); } - (executionPayload as electra.ExecutionPayload).exits = exits.map(deserializeExecutionLayerExit); + (executionPayload as electra.ExecutionPayload).withdrawalRequests = withdrawalRequests.map( + deserializeExecutionLayerWithdrawalRequest + ); } return {executionPayload, executionPayloadValue, blobsBundle, shouldOverrideBuilder}; @@ -409,17 +411,23 @@ export function deserializeDepositReceipt(serialized: DepositReceiptRpc): electr } as electra.DepositReceipt; } -export function serializeExecutionLayerExit(exit: electra.ExecutionLayerExit): ExecutionLayerExitRpc { +export function serializeExecutionLayerWithdrawalRequest( + withdrawalRequest: electra.ExecutionLayerWithdrawalRequest +): ExecutionLayerWithdrawalRequestRpc { return { - sourceAddress: bytesToData(exit.sourceAddress), - validatorPubkey: bytesToData(exit.validatorPubkey), + sourceAddress: bytesToData(withdrawalRequest.sourceAddress), + validatorPubkey: bytesToData(withdrawalRequest.validatorPubkey), + amount: numToQuantity(withdrawalRequest.amount), }; } -export function deserializeExecutionLayerExit(exit: ExecutionLayerExitRpc): electra.ExecutionLayerExit { +export function deserializeExecutionLayerWithdrawalRequest( + withdrawalRequest: ExecutionLayerWithdrawalRequestRpc +): electra.ExecutionLayerWithdrawalRequest { return { - sourceAddress: dataToBytes(exit.sourceAddress, 20), - validatorPubkey: dataToBytes(exit.validatorPubkey, 48), + sourceAddress: dataToBytes(withdrawalRequest.sourceAddress, 20), + validatorPubkey: dataToBytes(withdrawalRequest.validatorPubkey, 48), + amount: quantityToNum(withdrawalRequest.amount), }; } @@ -429,7 +437,9 @@ export function deserializeExecutionPayloadBody(data: ExecutionPayloadBodyRpc | transactions: data.transactions.map((tran) => dataToBytes(tran, null)), withdrawals: data.withdrawals ? data.withdrawals.map(deserializeWithdrawal) : null, depositReceipts: data.depositReceipts ? data.depositReceipts.map(deserializeDepositReceipt) : null, - exits: data.exits ? data.exits.map(deserializeExecutionLayerExit) : null, + withdrawalRequests: data.withdrawalRequests + ? data.withdrawalRequests.map(deserializeExecutionLayerWithdrawalRequest) + : null, } : null; } @@ -440,7 +450,9 @@ 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, + withdrawalRequests: data.withdrawalRequests + ? data.withdrawalRequests.map(serializeExecutionLayerWithdrawalRequest) + : null, } : null; } diff --git a/packages/light-client/src/spec/utils.ts b/packages/light-client/src/spec/utils.ts index c8aa2795a957..0c1dccbb7038 100644 --- a/packages/light-client/src/spec/utils.ts +++ b/packages/light-client/src/spec/utils.ts @@ -108,8 +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(); + (upgradedHeader as electra.LightClientHeader).execution.withdrawalRequestsRoot = + ssz.electra.LightClientHeader.fields.execution.fields.withdrawalRequestsRoot.defaultValue(); // Break if no further upgrades is required else fall through if (ForkSeq[targetFork] <= ForkSeq.electra) break; @@ -149,7 +149,7 @@ export function isValidLightClientHeader(config: ChainForkConfig, header: allFor if (epoch < config.ELECTRA_FORK_EPOCH) { if ( (header as electra.LightClientHeader).execution.depositReceiptsRoot !== undefined || - (header as electra.LightClientHeader).execution.exitsRoot !== undefined + (header as electra.LightClientHeader).execution.withdrawalRequestsRoot !== undefined ) { return false; } diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index 3e56effc4138..b261e07959d0 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -95,7 +95,7 @@ export const { KZG_COMMITMENT_INCLUSION_PROOF_DEPTH, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD, - MAX_EXECUTION_LAYER_EXITS, + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, MAX_ATTESTER_SLASHINGS_ELECTRA, MAX_ATTESTATIONS_ELECTRA, } = activePreset; diff --git a/packages/params/src/presets/mainnet.ts b/packages/params/src/presets/mainnet.ts index 27cb7640b2dd..5343966a43f4 100644 --- a/packages/params/src/presets/mainnet.ts +++ b/packages/params/src/presets/mainnet.ts @@ -121,7 +121,7 @@ export const mainnetPreset: BeaconPreset = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 8192, - MAX_EXECUTION_LAYER_EXITS: 16, + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16, MAX_ATTESTER_SLASHINGS_ELECTRA: 1, MAX_ATTESTATIONS_ELECTRA: 8, }; diff --git a/packages/params/src/presets/minimal.ts b/packages/params/src/presets/minimal.ts index 022532a49e6f..e4938d501a51 100644 --- a/packages/params/src/presets/minimal.ts +++ b/packages/params/src/presets/minimal.ts @@ -122,7 +122,7 @@ export const minimalPreset: BeaconPreset = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 4, - MAX_EXECUTION_LAYER_EXITS: 16, + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16, MAX_ATTESTER_SLASHINGS_ELECTRA: 1, MAX_ATTESTATIONS_ELECTRA: 8, }; diff --git a/packages/params/src/types.ts b/packages/params/src/types.ts index 34f40a66707e..e5b85a9e2224 100644 --- a/packages/params/src/types.ts +++ b/packages/params/src/types.ts @@ -85,7 +85,7 @@ export type BeaconPreset = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: number; - MAX_EXECUTION_LAYER_EXITS: number; + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: number; MAX_ATTESTER_SLASHINGS_ELECTRA: number; MAX_ATTESTATIONS_ELECTRA: number; }; @@ -176,7 +176,7 @@ export const beaconPresetTypes: BeaconPresetTypes = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: "number", - MAX_EXECUTION_LAYER_EXITS: "number", + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: "number", MAX_ATTESTER_SLASHINGS_ELECTRA: "number", MAX_ATTESTATIONS_ELECTRA: "number", }; diff --git a/packages/state-transition/src/block/processExecutionLayerExit.ts b/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts similarity index 71% rename from packages/state-transition/src/block/processExecutionLayerExit.ts rename to packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts index 5068d9af8667..cfe719bf57f3 100644 --- a/packages/state-transition/src/block/processExecutionLayerExit.ts +++ b/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts @@ -6,22 +6,33 @@ import {isActiveValidator} from "../util/index.js"; import {CachedBeaconStateElectra} from "../types.js"; import {initiateValidatorExit} from "./index.js"; +const FULL_EXIT_REQUEST_AMOUNT = 0; /** * 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; - } +export function processExecutionLayerWithdrawalRequest( + state: CachedBeaconStateElectra, + withdrawalRequest: electra.ExecutionLayerWithdrawalRequest +): void { + const isFullExitRequest = withdrawalRequest.amount === FULL_EXIT_REQUEST_AMOUNT; + + if (isFullExitRequest) { + const validator = isValidExecutionLayerExit(state, withdrawalRequest); + if (validator === null) { + return; + } - initiateValidatorExit(state, validator); + initiateValidatorExit(state, validator); + } else { + // partial withdral request add codeblock + } } +// TODO electra : add pending withdrawal check before exit export function isValidExecutionLayerExit( state: CachedBeaconStateElectra, - exit: electra.ExecutionLayerExit + exit: electra.ExecutionLayerWithdrawalRequest ): CompositeViewDU | null { const {config, epochCtx} = state; const validatorIndex = epochCtx.getValidatorIndex(exit.validatorPubkey); diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index 0e8c66942faf..b4c8975f23bd 100644 --- a/packages/state-transition/src/block/processOperations.ts +++ b/packages/state-transition/src/block/processOperations.ts @@ -8,7 +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 {processExecutionLayerWithdrawalRequest} from "./processExecutionLayerWithdrawalRequest.js"; import {processBlsToExecutionChange} from "./processBlsToExecutionChange.js"; import {processDepositReceipt} from "./processDepositReceipt.js"; import {ProcessBlockOpts} from "./types.js"; @@ -19,7 +19,7 @@ export { processAttestations, processDeposit, processVoluntaryExit, - processExecutionLayerExit, + processExecutionLayerWithdrawalRequest, processBlsToExecutionChange, processDepositReceipt, }; @@ -55,8 +55,8 @@ export function processOperations( processVoluntaryExit(state, voluntaryExit, opts.verifySignatures); } if (fork >= ForkSeq.electra) { - for (const elExit of (body as electra.BeaconBlockBody).executionPayload.exits) { - processExecutionLayerExit(state as CachedBeaconStateElectra, elExit); + for (const elWithdrawalRequest of (body as electra.BeaconBlockBody).executionPayload.withdrawalRequests) { + processExecutionLayerWithdrawalRequest(state as CachedBeaconStateElectra, elWithdrawalRequest); } } diff --git a/packages/state-transition/src/slot/upgradeStateToElectra.ts b/packages/state-transition/src/slot/upgradeStateToElectra.ts index 369ab19c447b..f41c37af94aa 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 and exitsRoot set to zeros by default + // latestExecutionPayloadHeader's depositReceiptsRoot and withdrawalRequestsRoot 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 37295f1e3802..2789836d0e3b 100644 --- a/packages/state-transition/src/util/execution.ts +++ b/packages/state-transition/src/util/execution.ts @@ -173,9 +173,10 @@ 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 - ); + (bellatrixPayloadFields as electra.ExecutionPayloadHeader).withdrawalRequestsRoot = + ssz.electra.ExecutionLayerWithdrawalRequests.hashTreeRoot( + (payload as electra.ExecutionPayload).withdrawalRequests + ); } return bellatrixPayloadFields; diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index 3404baf04110..53195f30de11 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -16,7 +16,7 @@ import { MAX_COMMITTEES_PER_SLOT, MAX_ATTESTATIONS_ELECTRA, MAX_ATTESTER_SLASHINGS_ELECTRA, - MAX_EXECUTION_LAYER_EXITS, + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, } from "@lodestar/params"; import {ssz as primitiveSsz} from "../primitive/index.js"; import {ssz as phase0Ssz} from "../phase0/index.js"; @@ -117,20 +117,24 @@ export const DepositReceipt = new ContainerType( export const DepositReceipts = new ListCompositeType(DepositReceipt, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD); -export const ExecutionLayerExit = new ContainerType( +export const ExecutionLayerWithdrawalRequest = new ContainerType( { sourceAddress: ExecutionAddress, validatorPubkey: BLSPubkey, + amount: UintNum64, }, - {typeName: "ExecutionLayerExit", jsonCase: "eth2"} + {typeName: "ExecutionLayerWithdrawalRequest", jsonCase: "eth2"} +); +export const ExecutionLayerWithdrawalRequests = new ListCompositeType( + ExecutionLayerWithdrawalRequest, + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD ); -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 + withdrawalRequests: ExecutionLayerWithdrawalRequests, // New in ELECTRA }, {typeName: "ExecutionPayload", jsonCase: "eth2"} ); @@ -139,7 +143,7 @@ export const ExecutionPayloadHeader = new ContainerType( { ...denebSsz.ExecutionPayloadHeader.fields, depositReceiptsRoot: Root, // New in ELECTRA - exitsRoot: Root, // New in ELECTRA + withdrawalRequestsRoot: 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 2925885b3af3..d1d0109e6ae6 100644 --- a/packages/types/src/electra/types.ts +++ b/packages/types/src/electra/types.ts @@ -12,8 +12,8 @@ export type SignedAggregateAndProof = ValueOf; export type DepositReceipts = ValueOf; -export type ExecutionLayerExit = ValueOf; -export type ExecutionLayerExits = ValueOf; +export type ExecutionLayerWithdrawalRequest = ValueOf; +export type ExecutionLayerWithdrawalRequests = 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 1eda005b70c3..ff1c8c0fdc25 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -224,7 +224,7 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record Date: Tue, 7 May 2024 21:18:41 +0300 Subject: [PATCH 09/51] feat: implement maxEB EIP-7251 (#6539) * feat: implement EIP-6110 (#6042) * Add immutable in the dependencies * Initial change to pubkeyCache * Added todos * Moved unfinalized cache to epochCache * Move populating finalized cache to afterProcessEpoch * Specify unfinalized cache during state cloning * Move from unfinalized to finalized cache in afterProcessEpoch * Confused myself * Clean up * Change logic * Fix cloning issue * Clean up redundant code * Add CarryoverData in epochCtx.createFromState * Fix typo * Update usage of pubkeyCache * Update pubkeyCache usage * Fix lint * Fix lint * Add 6110 to ChainConfig * Add 6110 to BeaconPreset * Define 6110 fork and container * Add V6110 api to execution engine * Update test * Add depositReceiptsRoot to process_execution_payload * State transitioning to EIP6110 * State transitioning to EIP6110 * Light client change in EIP-6110 * Update tests * produceBlock * Refactor processDeposit to match the spec * Implement processDepositReceipt * Implement 6110 fork guard for pubkeyCache * Handle changes in eth1 deposit * Update eth1 deposit test * Fix typo * Lint * Remove embarassing comments * Address comments * Modify applyDeposit signature * Update packages/state-transition/src/cache/epochCache.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Update packages/state-transition/src/cache/epochCache.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Update packages/state-transition/src/cache/pubkeyCache.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Remove old code * Rename fields in epochCache and immutableData * Remove CarryoverData * Move isAfter6110 from var to method * Fix cyclic import * Fix operations spec runner * Fix for spec test * Fix spec test * state.depositReceiptsStartIndex to BigInt * getDeposit requires cached state * default depositReceiptsStartIndex value in genesis * Fix pubkeyCache bug * newUnfinalizedPubkeyIndexMap in createCachedBeaconState * Lint * Pass epochCache instead of pubkey2IndexFn in apis * Address comments * Add unit test on pubkey cache cloning * Add unfinalizedPubkeyCacheSize to metrics * Add unfinalizedPubkeyCacheSize to metrics * Clean up code * Add besu to el-interop * Add 6110 genesis file * Template for sim test * Add unit test for getEth1DepositCount * Update sim test * Update besudocker * Finish beacon api calls in sim test * Update epochCache.createFromState() * Fix bug unfinalized validators are not finalized * Add sim test to run a few blocks * Lint * Merge branch 'unstable' into 611 * Add more check to sim test * Update besu docker image instruction * Update sim test with correct tx * Address comment + cleanup * Clean up code * Properly handle promise rejection * Lint * Update packages/beacon-node/src/execution/engine/types.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Update comments * Accept type undefined in ExecutionPayloadBodyRpc * Update comment and semantic * Remove if statement when adding finalized validator * Comment on repeated insert on finalized cache * rename createFromState * Add comment on getPubkey() * Stash change to reduce diffs * Stash change to reduce diffs * Lint * addFinalizedPubkey on finalized checkpoint * Update comment * Use OrderedMap for unfinalized cache * Pull out logic of deleting pubkeys for batch op * Add updateUnfinalizedPubkeys in regen * Update updateUnfinalizedPubkeys logic * Add comment * Add metrics for state context caches * Address comment * Address comment * Deprecate eth1Data polling when condition is reached * Fix conflicts * Fix sim test * Lint * Fix type * Fix test * Fix test * Lint * Update packages/light-client/src/spec/utils.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Fix spec test * Address comments * Improve cache logic on checkpoint finalized * Update sim test according to new cache logic * Update comment * Lint * Finalized pubkey cache only update once per checkpoint * Add perf test for updateUnfinalizedPubkeys * Add perf test for updateUnfinalizedPubkeys * Tweak params for perf test * Freeze besu docker image version for 6110 * Add benchmark result * Use Map instead of OrderedMap. Update benchmark * Minor optimization * Minor optimization * Add memory test for immutable.js * Update test * Reduce code duplication * Lint * Remove try/catch in updateUnfinalizedPubkeys * Introduce EpochCache metric * Add historicalValidatorLengths * Polish code * Migrate state-transition unit tests to vitest * Fix calculation of pivot index * `historicalValidatorLengths` only activate post 6110 * Update sim test * Lint * Update packages/state-transition/src/cache/epochCache.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Improve readability on historicalValidatorLengths * Update types * Fix calculation * Add eth1data poll todo * Add epochCache.getValidatorCountAtEpoch * Add todo * Add getStateIterator for state cache * Partial commit * Update perf test * updateUnfinalizedPubkeys directly modify states from regen * Update sim test. Lint * Add todo * some improvements and a fix for effectiveBalanceIncrements fork safeness * rename eip6110 to elctra * fix electra-interop.test.ts --------- Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Co-authored-by: gajinder lint and tsc small cleanup fix rebase issue * feat: implement EIP-6110 (#6042) * Add immutable in the dependencies * Initial change to pubkeyCache * Added todos * Moved unfinalized cache to epochCache * Move populating finalized cache to afterProcessEpoch * Specify unfinalized cache during state cloning * Move from unfinalized to finalized cache in afterProcessEpoch * Confused myself * Clean up * Change logic * Fix cloning issue * Clean up redundant code * Add CarryoverData in epochCtx.createFromState * Fix typo * Update usage of pubkeyCache * Update pubkeyCache usage * Fix lint * Fix lint * Add 6110 to ChainConfig * Add 6110 to BeaconPreset * Define 6110 fork and container * Add V6110 api to execution engine * Update test * Add depositReceiptsRoot to process_execution_payload * State transitioning to EIP6110 * State transitioning to EIP6110 * Light client change in EIP-6110 * Update tests * produceBlock * Refactor processDeposit to match the spec * Implement processDepositReceipt * Implement 6110 fork guard for pubkeyCache * Handle changes in eth1 deposit * Update eth1 deposit test * Fix typo * Lint * Remove embarassing comments * Address comments * Modify applyDeposit signature * Update packages/state-transition/src/cache/epochCache.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Update packages/state-transition/src/cache/epochCache.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Update packages/state-transition/src/cache/pubkeyCache.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Remove old code * Rename fields in epochCache and immutableData * Remove CarryoverData * Move isAfter6110 from var to method * Fix cyclic import * Fix operations spec runner * Fix for spec test * Fix spec test * state.depositReceiptsStartIndex to BigInt * getDeposit requires cached state * default depositReceiptsStartIndex value in genesis * Fix pubkeyCache bug * newUnfinalizedPubkeyIndexMap in createCachedBeaconState * Lint * Pass epochCache instead of pubkey2IndexFn in apis * Address comments * Add unit test on pubkey cache cloning * Add unfinalizedPubkeyCacheSize to metrics * Add unfinalizedPubkeyCacheSize to metrics * Clean up code * Add besu to el-interop * Add 6110 genesis file * Template for sim test * Add unit test for getEth1DepositCount * Update sim test * Update besudocker * Finish beacon api calls in sim test * Update epochCache.createFromState() * Fix bug unfinalized validators are not finalized * Add sim test to run a few blocks * Lint * Merge branch 'unstable' into 611 * Add more check to sim test * Update besu docker image instruction * Update sim test with correct tx * Address comment + cleanup * Clean up code * Properly handle promise rejection * Lint * Update packages/beacon-node/src/execution/engine/types.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Update comments * Accept type undefined in ExecutionPayloadBodyRpc * Update comment and semantic * Remove if statement when adding finalized validator * Comment on repeated insert on finalized cache * rename createFromState * Add comment on getPubkey() * Stash change to reduce diffs * Stash change to reduce diffs * Lint * addFinalizedPubkey on finalized checkpoint * Update comment * Use OrderedMap for unfinalized cache * Pull out logic of deleting pubkeys for batch op * Add updateUnfinalizedPubkeys in regen * Update updateUnfinalizedPubkeys logic * Add comment * Add metrics for state context caches * Address comment * Address comment * Deprecate eth1Data polling when condition is reached * Fix conflicts * Fix sim test * Lint * Fix type * Fix test * Fix test * Lint * Update packages/light-client/src/spec/utils.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Fix spec test * Address comments * Improve cache logic on checkpoint finalized * Update sim test according to new cache logic * Update comment * Lint * Finalized pubkey cache only update once per checkpoint * Add perf test for updateUnfinalizedPubkeys * Add perf test for updateUnfinalizedPubkeys * Tweak params for perf test * Freeze besu docker image version for 6110 * Add benchmark result * Use Map instead of OrderedMap. Update benchmark * Minor optimization * Minor optimization * Add memory test for immutable.js * Update test * Reduce code duplication * Lint * Remove try/catch in updateUnfinalizedPubkeys * Introduce EpochCache metric * Add historicalValidatorLengths * Polish code * Migrate state-transition unit tests to vitest * Fix calculation of pivot index * `historicalValidatorLengths` only activate post 6110 * Update sim test * Lint * Update packages/state-transition/src/cache/epochCache.ts Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> * Improve readability on historicalValidatorLengths * Update types * Fix calculation * Add eth1data poll todo * Add epochCache.getValidatorCountAtEpoch * Add todo * Add getStateIterator for state cache * Partial commit * Update perf test * updateUnfinalizedPubkeys directly modify states from regen * Update sim test. Lint * Add todo * some improvements and a fix for effectiveBalanceIncrements fork safeness * rename eip6110 to elctra * fix electra-interop.test.ts --------- Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Co-authored-by: gajinder lint and tsc small cleanup * Add presets * Update config * Add necessary containers * Update presets * Update config * Add todo comments * Update constants and params * Impl new process withdrawal * Add withdrawaRequests to payload * Add processConsolidation * Add process withdraw request * Update deposit and withdrawal flow * epoch processing * Implement churn limits * Lint * lint * Update state-transition utils * processExecutionLayerWithdrawRequest * processConsolidation * queueExcessActiveBalance * isValidDepositSignature * Add jsdoc and timer for new processEpoch functions * Lint * Update maxEB * update voluntary exit * Fix config * Update initiateValidatorExit * Remove churn limit in processRegistryUpdates * Fix conflict * Add MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD * Reflect latest spec changes * rebase fixes, fixes, improvements and cleanup lint * Upgrade ssz version * Use sliceFrom() * cleanup as per specs feedback subarry * simplify * fix withdrawals * remove slice * fix the slashing quotient determination in slashvalidator --------- Co-authored-by: harkamal --- packages/api/package.json | 2 +- packages/beacon-node/package.json | 2 +- .../chain/produceBlock/produceBlockBody.ts | 2 + .../test/sim/electra-interop.test.ts | 2 +- .../test/spec/presets/operations.test.ts | 4 +- .../test/unit/executionEngine/http.test.ts | 8 +- packages/cli/package.json | 2 +- packages/config/package.json | 2 +- .../config/src/chainConfig/configs/mainnet.ts | 6 + .../config/src/chainConfig/configs/minimal.ts | 6 + packages/config/src/chainConfig/types.ts | 4 + packages/db/package.json | 2 +- packages/fork-choice/package.json | 2 +- packages/light-client/package.json | 2 +- packages/params/src/index.ts | 14 +- packages/params/src/presets/mainnet.ts | 11 ++ packages/params/src/presets/minimal.ts | 13 +- packages/params/src/types.ts | 18 +++ packages/state-transition/package.json | 6 +- packages/state-transition/src/block/index.ts | 2 + .../src/block/initiateValidatorExit.ts | 39 ++++-- .../src/block/processConsolidation.ts | 111 +++++++++++++++ .../src/block/processDeposit.ts | 98 ++++++++++---- .../processExecutionLayerWithdrawalRequest.ts | 126 +++++++++++------- .../src/block/processOperations.ts | 25 ++-- .../src/block/processVoluntaryExit.ts | 30 ++++- .../src/block/processWithdrawals.ts | 87 +++++++++--- .../src/block/slashValidator.ts | 15 ++- .../state-transition/src/cache/epochCache.ts | 2 + .../src/cache/epochTransitionCache.ts | 8 +- packages/state-transition/src/epoch/index.ts | 32 ++++- .../epoch/processEffectiveBalanceUpdates.ts | 22 ++- .../epoch/processPendingBalanceDeposits.ts | 35 +++++ .../src/epoch/processPendingConsolidations.ts | 45 +++++++ .../src/epoch/processRegistryUpdates.ts | 20 ++- .../src/signatureSets/consolidation.ts | 51 +++++++ .../src/signatureSets/index.ts | 16 ++- .../src/slot/upgradeStateToElectra.ts | 24 +++- packages/state-transition/src/util/electra.ts | 103 ++++++++++++++ packages/state-transition/src/util/epoch.ts | 58 +++++++- packages/state-transition/src/util/genesis.ts | 2 +- packages/state-transition/src/util/index.ts | 1 + .../state-transition/src/util/validator.ts | 55 +++++++- .../test/perf/analyzeEpochs.ts | 3 + .../perf/block/processWithdrawals.test.ts | 5 +- .../unit/block/processWithdrawals.test.ts | 4 +- packages/types/package.json | 2 +- packages/types/src/electra/sszTypes.ts | 60 ++++++++- packages/types/src/electra/types.ts | 7 + packages/types/src/phase0/sszTypes.ts | 2 +- packages/validator/package.json | 4 +- packages/validator/src/util/params.ts | 11 ++ .../test/unit/utils/interopConfigs.ts | 34 +++++ yarn.lock | 25 +++- 54 files changed, 1094 insertions(+), 178 deletions(-) create mode 100644 packages/state-transition/src/block/processConsolidation.ts create mode 100644 packages/state-transition/src/epoch/processPendingBalanceDeposits.ts create mode 100644 packages/state-transition/src/epoch/processPendingConsolidations.ts create mode 100644 packages/state-transition/src/signatureSets/consolidation.ts create mode 100644 packages/state-transition/src/util/electra.ts diff --git a/packages/api/package.json b/packages/api/package.json index 67b966fcfb30..e0687cd4b8b1 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -71,7 +71,7 @@ }, "dependencies": { "@chainsafe/persistent-merkle-tree": "^0.7.1", - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "^0.16.0", "@lodestar/config": "^1.19.0", "@lodestar/params": "^1.19.0", "@lodestar/types": "^1.19.0", diff --git a/packages/beacon-node/package.json b/packages/beacon-node/package.json index ee17ff0c0c08..4c67bae11b22 100644 --- a/packages/beacon-node/package.json +++ b/packages/beacon-node/package.json @@ -104,7 +104,7 @@ "@chainsafe/libp2p-noise": "^15.0.0", "@chainsafe/persistent-merkle-tree": "^0.7.1", "@chainsafe/prometheus-gc-stats": "^1.0.0", - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "^0.16.0", "@chainsafe/threads": "^1.11.1", "@ethersproject/abi": "^5.7.0", "@fastify/bearer-auth": "^9.0.0", diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index c1c1d5eaa447..988e3ca135e1 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -556,7 +556,9 @@ function preparePayloadAttributes( }; if (ForkSeq[fork] >= ForkSeq.capella) { + // withdrawals logic is now fork aware as it changes on electra fork post capella (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = getExpectedWithdrawals( + ForkSeq[fork], prepareState as CachedBeaconStateCapella ).withdrawals; } diff --git a/packages/beacon-node/test/sim/electra-interop.test.ts b/packages/beacon-node/test/sim/electra-interop.test.ts index ab3f1ae6b2c1..29483b249c85 100644 --- a/packages/beacon-node/test/sim/electra-interop.test.ts +++ b/packages/beacon-node/test/sim/electra-interop.test.ts @@ -173,7 +173,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { blockHash: dataToBytes(newPayloadBlockHash, 32), receiptsRoot: dataToBytes("0x79ee3424eb720a3ad4b1c5a372bb8160580cbe4d893778660f34213c685627a9", 32), blobGasUsed: 0n, - exits: [], + withdrawalRequests: [], }; const parentBeaconBlockRoot = dataToBytes("0x0000000000000000000000000000000000000000000000000000000000000000", 32); const payloadResult = await executionEngine.notifyNewPayload( diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index 2c4ae22475a5..a96d38298542 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -11,7 +11,7 @@ import { import * as blockFns from "@lodestar/state-transition/block"; import {ssz, phase0, altair, bellatrix, capella, electra} from "@lodestar/types"; import {InputType} from "@lodestar/spec-test-util"; -import {ACTIVE_PRESET, ForkName} from "@lodestar/params"; +import {ACTIVE_PRESET, ForkName, ForkSeq} from "@lodestar/params"; import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js"; import {expectEqualBeaconState, inputTypeSszTreeViewDU} from "../utils/expectEqualBeaconState.js"; @@ -88,7 +88,7 @@ const operationFns: Record> = }, withdrawals: (state, testCase: {execution_payload: capella.ExecutionPayload}) => { - blockFns.processWithdrawals(state as CachedBeaconStateCapella, testCase.execution_payload); + blockFns.processWithdrawals(ForkSeq.capella, state as CachedBeaconStateCapella, testCase.execution_payload); }, }; diff --git a/packages/beacon-node/test/unit/executionEngine/http.test.ts b/packages/beacon-node/test/unit/executionEngine/http.test.ts index 29bced11b236..5721b9ce5ad1 100644 --- a/packages/beacon-node/test/unit/executionEngine/http.test.ts +++ b/packages/beacon-node/test/unit/executionEngine/http.test.ts @@ -189,7 +189,7 @@ describe("ExecutionEngine / http", () => { }, ], depositReceipts: null, // depositReceipts is null pre-electra - exits: null, + withdrawalRequests: null, }, null, // null returned for missing blocks { @@ -199,7 +199,7 @@ describe("ExecutionEngine / http", () => { ], withdrawals: null, // withdrawals is null pre-capella depositReceipts: null, // depositReceipts is null pre-electra - exits: null, + withdrawalRequests: null, }, ], }; @@ -248,7 +248,7 @@ describe("ExecutionEngine / http", () => { }, ], depositReceipts: null, // depositReceipts is null pre-electra - exits: null, + withdrawalRequests: null, }, null, // null returned for missing blocks { @@ -258,7 +258,7 @@ describe("ExecutionEngine / http", () => { ], withdrawals: null, // withdrawals is null pre-capella depositReceipts: null, // depositReceipts is null pre-electra - exits: null, + withdrawalRequests: null, }, ], }; diff --git a/packages/cli/package.json b/packages/cli/package.json index 6a2f9d04644d..b535bd9ca623 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -58,7 +58,7 @@ "@chainsafe/discv5": "^9.0.0", "@chainsafe/enr": "^3.0.0", "@chainsafe/persistent-merkle-tree": "^0.7.1", - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "^0.16.0", "@chainsafe/threads": "^1.11.1", "@libp2p/crypto": "^4.1.0", "@libp2p/peer-id": "^4.1.0", diff --git a/packages/config/package.json b/packages/config/package.json index bc8f880cb7a8..e81fb7274005 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -64,7 +64,7 @@ "blockchain" ], "dependencies": { - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "^0.16.0", "@lodestar/params": "^1.19.0", "@lodestar/types": "^1.19.0" } diff --git a/packages/config/src/chainConfig/configs/mainnet.ts b/packages/config/src/chainConfig/configs/mainnet.ts index 6d1fd9b75952..741ddc99f8cd 100644 --- a/packages/config/src/chainConfig/configs/mainnet.ts +++ b/packages/config/src/chainConfig/configs/mainnet.ts @@ -102,4 +102,10 @@ export const chainConfig: ChainConfig = { // Deneb // `2**12` (= 4096 epochs, ~18 days) MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096, + + // Electra + // 2**8 * 10**9 (= 256,000,000,000) + MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 256000000000, + // 2*7 * 10**9 (= 128,000,000,000) + MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 128000000000, }; diff --git a/packages/config/src/chainConfig/configs/minimal.ts b/packages/config/src/chainConfig/configs/minimal.ts index 44a28ca36ec7..26f49cc3e47d 100644 --- a/packages/config/src/chainConfig/configs/minimal.ts +++ b/packages/config/src/chainConfig/configs/minimal.ts @@ -99,4 +99,10 @@ export const chainConfig: ChainConfig = { // Deneb // `2**12` (= 4096 epochs, ~18 days) MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096, + + // Electra + // 2**7 * 10**9 (= 128,000,000,000) + MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: 128000000000, + // 2**6 * 10**9 (= 64,000,000,000) + MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: 64000000000, }; diff --git a/packages/config/src/chainConfig/types.ts b/packages/config/src/chainConfig/types.ts index 234a08558be5..05fff02f2eaf 100644 --- a/packages/config/src/chainConfig/types.ts +++ b/packages/config/src/chainConfig/types.ts @@ -58,6 +58,8 @@ export type ChainConfig = { MIN_PER_EPOCH_CHURN_LIMIT: number; MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: number; CHURN_LIMIT_QUOTIENT: number; + MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: number; + MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: number; // Fork choice PROPOSER_SCORE_BOOST: number; @@ -120,6 +122,8 @@ export const chainConfigTypes: SpecTypes = { MIN_PER_EPOCH_CHURN_LIMIT: "number", MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: "number", CHURN_LIMIT_QUOTIENT: "number", + MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT: "number", + MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA: "number", // Fork choice PROPOSER_SCORE_BOOST: "number", diff --git a/packages/db/package.json b/packages/db/package.json index 2ff5421c7ea9..0bfdd3f573cc 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -35,7 +35,7 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "^0.16.0", "@lodestar/config": "^1.19.0", "@lodestar/utils": "^1.19.0", "classic-level": "^1.4.1", diff --git a/packages/fork-choice/package.json b/packages/fork-choice/package.json index 1a1b20bfe6e1..aee297d29432 100644 --- a/packages/fork-choice/package.json +++ b/packages/fork-choice/package.json @@ -36,7 +36,7 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "^0.16.0", "@lodestar/config": "^1.19.0", "@lodestar/params": "^1.19.0", "@lodestar/state-transition": "^1.19.0", diff --git a/packages/light-client/package.json b/packages/light-client/package.json index 4f79eeb61274..46b4c7d1df27 100644 --- a/packages/light-client/package.json +++ b/packages/light-client/package.json @@ -74,7 +74,7 @@ "dependencies": { "@chainsafe/bls": "7.1.3", "@chainsafe/persistent-merkle-tree": "^0.7.1", - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "^0.16.0", "@lodestar/api": "^1.19.0", "@lodestar/config": "^1.19.0", "@lodestar/params": "^1.19.0", diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index b261e07959d0..c1d3f1bc1981 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -94,10 +94,20 @@ export const { MAX_BLOBS_PER_BLOCK, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH, + MAX_EFFECTIVE_BALANCE_ELECTRA, + MIN_ACTIVATION_BALANCE, + PENDING_BALANCE_DEPOSITS_LIMIT, + PENDING_PARTIAL_WITHDRAWALS_LIMIT, + PENDING_CONSOLIDATIONS_LIMIT, + MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA, + MAX_CONSOLIDATIONS, + MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, MAX_ATTESTER_SLASHINGS_ELECTRA, MAX_ATTESTATIONS_ELECTRA, + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP, + WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA, } = activePreset; //////////// @@ -119,6 +129,7 @@ export const JUSTIFICATION_BITS_LENGTH = 4; // Since the prefixes are just 1 byte, we define and use them as number export const BLS_WITHDRAWAL_PREFIX = 0; export const ETH1_ADDRESS_WITHDRAWAL_PREFIX = 1; +export const COMPOUNDING_WITHDRAWAL_PREFIX = 2; // Domain types @@ -133,7 +144,7 @@ export const DOMAIN_SYNC_COMMITTEE = Uint8Array.from([7, 0, 0, 0]); export const DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF = Uint8Array.from([8, 0, 0, 0]); export const DOMAIN_CONTRIBUTION_AND_PROOF = Uint8Array.from([9, 0, 0, 0]); export const DOMAIN_BLS_TO_EXECUTION_CHANGE = Uint8Array.from([10, 0, 0, 0]); -export const DOMAIN_BLOB_SIDECAR = Uint8Array.from([11, 0, 0, 0]); +export const DOMAIN_CONSOLIDATION = Uint8Array.from([11, 0, 0, 0]); // Application specific domains @@ -252,3 +263,4 @@ export const BLOBSIDECAR_FIXED_SIZE = ACTIVE_PRESET === PresetName.minimal ? 131 // Electra Misc export const UNSET_DEPOSIT_RECEIPTS_START_INDEX = 2n ** 64n - 1n; +export const FULL_EXIT_REQUEST_AMOUNT = 0; diff --git a/packages/params/src/presets/mainnet.ts b/packages/params/src/presets/mainnet.ts index 5343966a43f4..2495f7ef97a1 100644 --- a/packages/params/src/presets/mainnet.ts +++ b/packages/params/src/presets/mainnet.ts @@ -124,4 +124,15 @@ export const mainnetPreset: BeaconPreset = { MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16, MAX_ATTESTER_SLASHINGS_ELECTRA: 1, MAX_ATTESTATIONS_ELECTRA: 8, + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 8, + // 2**11 * 10**9 (= 2,048,000,000,000) Gwei + MAX_EFFECTIVE_BALANCE_ELECTRA: 2048000000000, + // 2**16 (= 65536) + MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: 4096, + MIN_ACTIVATION_BALANCE: 32000000000, + PENDING_BALANCE_DEPOSITS_LIMIT: 134217728, + PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728, + PENDING_CONSOLIDATIONS_LIMIT: 262144, + MAX_CONSOLIDATIONS: 1, + WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: 4096, }; diff --git a/packages/params/src/presets/minimal.ts b/packages/params/src/presets/minimal.ts index e4938d501a51..8e71407965d7 100644 --- a/packages/params/src/presets/minimal.ts +++ b/packages/params/src/presets/minimal.ts @@ -122,7 +122,18 @@ export const minimalPreset: BeaconPreset = { // ELECTRA MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD: 4, - MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16, + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 2, MAX_ATTESTER_SLASHINGS_ELECTRA: 1, MAX_ATTESTATIONS_ELECTRA: 8, + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 1, + // 2**11 * 10**9 (= 2,048,000,000,000) Gwei + MAX_EFFECTIVE_BALANCE_ELECTRA: 2048000000000, + // 2**16 (= 65536) + MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: 4096, + MIN_ACTIVATION_BALANCE: 32000000000, + PENDING_BALANCE_DEPOSITS_LIMIT: 134217728, + PENDING_PARTIAL_WITHDRAWALS_LIMIT: 64, + PENDING_CONSOLIDATIONS_LIMIT: 64, + MAX_CONSOLIDATIONS: 1, + WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: 4096, }; diff --git a/packages/params/src/types.ts b/packages/params/src/types.ts index e5b85a9e2224..dffd98518006 100644 --- a/packages/params/src/types.ts +++ b/packages/params/src/types.ts @@ -88,6 +88,15 @@ export type BeaconPreset = { MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: number; MAX_ATTESTER_SLASHINGS_ELECTRA: number; MAX_ATTESTATIONS_ELECTRA: number; + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: number; + MAX_EFFECTIVE_BALANCE_ELECTRA: number; + MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: number; + MIN_ACTIVATION_BALANCE: number; + PENDING_BALANCE_DEPOSITS_LIMIT: number; + PENDING_PARTIAL_WITHDRAWALS_LIMIT: number; + PENDING_CONSOLIDATIONS_LIMIT: number; + MAX_CONSOLIDATIONS: number; + WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: number; }; /** @@ -179,6 +188,15 @@ export const beaconPresetTypes: BeaconPresetTypes = { MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: "number", MAX_ATTESTER_SLASHINGS_ELECTRA: "number", MAX_ATTESTATIONS_ELECTRA: "number", + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: "number", + MAX_EFFECTIVE_BALANCE_ELECTRA: "number", + MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA: "number", + MIN_ACTIVATION_BALANCE: "number", + PENDING_BALANCE_DEPOSITS_LIMIT: "number", + PENDING_PARTIAL_WITHDRAWALS_LIMIT: "number", + PENDING_CONSOLIDATIONS_LIMIT: "number", + MAX_CONSOLIDATIONS: "number", + WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA: "number", }; type BeaconPresetTypes = { diff --git a/packages/state-transition/package.json b/packages/state-transition/package.json index 674058780101..5a3b44f507f2 100644 --- a/packages/state-transition/package.json +++ b/packages/state-transition/package.json @@ -59,11 +59,11 @@ "types": "lib/index.d.ts", "dependencies": { "@chainsafe/as-sha256": "^0.4.1", - "@chainsafe/bls": "7.1.3", - "@chainsafe/blst": "^0.2.11", + "@chainsafe/bls": "^8.1.0", + "@chainsafe/blst": "^1.0.0", "@chainsafe/persistent-merkle-tree": "^0.7.1", "@chainsafe/persistent-ts": "^0.19.1", - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "^0.16.0", "@lodestar/config": "^1.19.0", "@lodestar/params": "^1.19.0", "@lodestar/types": "^1.19.0", diff --git a/packages/state-transition/src/block/index.ts b/packages/state-transition/src/block/index.ts index b235f7ca24ef..3c3361ea7bc0 100644 --- a/packages/state-transition/src/block/index.ts +++ b/packages/state-transition/src/block/index.ts @@ -47,10 +47,12 @@ export function processBlock( // https://github.com/ethereum/consensus-specs/blob/b62c9e877990242d63aa17a2a59a49bc649a2f2e/specs/eip4844/beacon-chain.md#disabling-withdrawals if (fork >= ForkSeq.capella) { processWithdrawals( + fork, state as CachedBeaconStateCapella, fullOrBlindedPayload as capella.FullOrBlindedExecutionPayload ); } + processExecutionPayload(fork, state as CachedBeaconStateBellatrix, block.body, externalData); } diff --git a/packages/state-transition/src/block/initiateValidatorExit.ts b/packages/state-transition/src/block/initiateValidatorExit.ts index e34d4dda7002..d1420daef84c 100644 --- a/packages/state-transition/src/block/initiateValidatorExit.ts +++ b/packages/state-transition/src/block/initiateValidatorExit.ts @@ -1,7 +1,8 @@ import {CompositeViewDU} from "@chainsafe/ssz"; -import {FAR_FUTURE_EPOCH} from "@lodestar/params"; +import {FAR_FUTURE_EPOCH, ForkSeq} from "@lodestar/params"; import {ssz} from "@lodestar/types"; -import {CachedBeaconStateAllForks} from "../types.js"; +import {CachedBeaconStateAllForks, CachedBeaconStateElectra} from "../types.js"; +import {computeExitEpochAndUpdateChurn} from "../util/epoch.js"; /** * Initiate the exit of the validator with index ``index``. @@ -24,6 +25,7 @@ import {CachedBeaconStateAllForks} from "../types.js"; * Forcing consumers to pass the SubTree of `validator` directly mitigates this issue. */ export function initiateValidatorExit( + fork: ForkSeq, state: CachedBeaconStateAllForks, validator: CompositeViewDU ): void { @@ -34,18 +36,27 @@ export function initiateValidatorExit( return; } - // Limits the number of validators that can exit on each epoch. - // Expects all state.validators to follow this rule, i.e. no validator.exitEpoch is greater than exitQueueEpoch. - // If there the churnLimit is reached at this current exitQueueEpoch, advance epoch and reset churn. - if (epochCtx.exitQueueChurn >= epochCtx.churnLimit) { - epochCtx.exitQueueEpoch += 1; - epochCtx.exitQueueChurn = 1; // = 1 to account for this validator with exitQueueEpoch + if (fork < ForkSeq.electra) { + // Limits the number of validators that can exit on each epoch. + // Expects all state.validators to follow this rule, i.e. no validator.exitEpoch is greater than exitQueueEpoch. + // If there the churnLimit is reached at this current exitQueueEpoch, advance epoch and reset churn. + if (epochCtx.exitQueueChurn >= epochCtx.churnLimit) { + epochCtx.exitQueueEpoch += 1; + epochCtx.exitQueueChurn = 1; // = 1 to account for this validator with exitQueueEpoch + } else { + // Add this validator to the current exitQueueEpoch churn + epochCtx.exitQueueChurn += 1; + } + + // set validator exit epoch + validator.exitEpoch = epochCtx.exitQueueEpoch; } else { - // Add this validator to the current exitQueueEpoch churn - epochCtx.exitQueueChurn += 1; + // set validator exit epoch + // Note we don't use epochCtx.exitQueueChurn and exitQueueEpoch anymore + validator.exitEpoch = computeExitEpochAndUpdateChurn( + state as CachedBeaconStateElectra, + BigInt(validator.effectiveBalance) + ); } - - // set validator exit epoch and withdrawable epoch - validator.exitEpoch = epochCtx.exitQueueEpoch; - validator.withdrawableEpoch = epochCtx.exitQueueEpoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY; + validator.withdrawableEpoch = validator.exitEpoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY; } diff --git a/packages/state-transition/src/block/processConsolidation.ts b/packages/state-transition/src/block/processConsolidation.ts new file mode 100644 index 000000000000..846b0e2521b4 --- /dev/null +++ b/packages/state-transition/src/block/processConsolidation.ts @@ -0,0 +1,111 @@ +import {toHexString} from "@chainsafe/ssz"; +import {electra, ssz} from "@lodestar/types"; +import {FAR_FUTURE_EPOCH, MIN_ACTIVATION_BALANCE, PENDING_CONSOLIDATIONS_LIMIT} from "@lodestar/params"; +import {verifyConsolidationSignature} from "../signatureSets/index.js"; + +import {CachedBeaconStateElectra} from "../types.js"; +import {getConsolidationChurnLimit, isActiveValidator} from "../util/validator.js"; +import {hasExecutionWithdrawalCredential} from "../util/electra.js"; +import {computeConsolidationEpochAndUpdateChurn} from "../util/epoch.js"; + +export function processConsolidation( + state: CachedBeaconStateElectra, + signedConsolidation: electra.SignedConsolidation +): void { + assertValidConsolidation(state, signedConsolidation); + + // Initiate source validator exit and append pending consolidation + const {sourceIndex, targetIndex} = signedConsolidation.message; + const sourceValidator = state.validators.get(sourceIndex); + + const exitEpoch = computeConsolidationEpochAndUpdateChurn(state, BigInt(sourceValidator.effectiveBalance)); + sourceValidator.exitEpoch = exitEpoch; + sourceValidator.withdrawableEpoch = exitEpoch + state.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY; + + const pendingConsolidation = ssz.electra.PendingConsolidation.toViewDU({ + sourceIndex, + targetIndex, + }); + state.pendingConsolidations.push(pendingConsolidation); +} + +function assertValidConsolidation( + state: CachedBeaconStateElectra, + signedConsolidation: electra.SignedConsolidation +): void { + // If the pending consolidations queue is full, no consolidations are allowed in the block + if (state.pendingConsolidations.length >= PENDING_CONSOLIDATIONS_LIMIT) { + throw new Error("Pending consolidation queue is full"); + } + + // If there is too little available consolidation churn limit, no consolidations are allowed in the block + // assert get_consolidation_churn_limit(state) > MIN_ACTIVATION_BALANCE + if (getConsolidationChurnLimit(state) <= MIN_ACTIVATION_BALANCE) { + throw new Error(`Consolidation churn limit too low. consolidationChurnLimit=${getConsolidationChurnLimit(state)}`); + } + + const consolidation = signedConsolidation.message; + const {sourceIndex, targetIndex} = consolidation; + + // Verify that source != target, so a consolidation cannot be used as an exit. + if (sourceIndex === targetIndex) { + throw new Error( + `Consolidation source and target index cannot be the same: sourceIndex=${sourceIndex} targetIndex=${targetIndex}` + ); + } + + const sourceValidator = state.validators.getReadonly(sourceIndex); + const targetValidator = state.validators.getReadonly(targetIndex); + const currentEpoch = state.epochCtx.epoch; + + // Verify the source and the target are active + if (!isActiveValidator(sourceValidator, currentEpoch)) { + throw new Error(`Consolidation source validator is not active: sourceIndex=${sourceIndex}`); + } + + if (!isActiveValidator(targetValidator, currentEpoch)) { + throw new Error(`Consolidation target validator is not active: targetIndex=${targetIndex}`); + } + + // Verify exits for source and target have not been initiated + if (sourceValidator.exitEpoch !== FAR_FUTURE_EPOCH) { + throw new Error(`Consolidation source validator has initialized exit: sourceIndex=${sourceIndex}`); + } + if (targetValidator.exitEpoch !== FAR_FUTURE_EPOCH) { + throw new Error(`Consolidation target validator has initialized exit: targetIndex=${targetIndex}`); + } + + // Consolidations must specify an epoch when they become valid; they are not valid before then + if (currentEpoch < consolidation.epoch) { + throw new Error( + `Consolidation epoch is after the current epoch: consolidationEpoch=${consolidation.epoch} currentEpoch=${currentEpoch}` + ); + } + + // Verify the source and the target have Execution layer withdrawal credentials + if (!hasExecutionWithdrawalCredential(sourceValidator.withdrawalCredentials)) { + throw new Error( + `Consolidation source validator does not have execution withdrawal credentials: sourceIndex=${sourceIndex}` + ); + } + if (!hasExecutionWithdrawalCredential(targetValidator.withdrawalCredentials)) { + throw new Error( + `Consolidation target validator does not have execution withdrawal credentials: targetIndex=${targetIndex}` + ); + } + + // Verify the same withdrawal address + const sourceWithdrawalAddress = toHexString(sourceValidator.withdrawalCredentials.subarray(12)); + const targetWithdrawalAddress = toHexString(targetValidator.withdrawalCredentials.subarray(12)); + + if (sourceWithdrawalAddress !== targetWithdrawalAddress) { + throw new Error( + `Consolidation source and target withdrawal address are different: source: ${sourceWithdrawalAddress} target: ${targetWithdrawalAddress}` + ); + } + + // Verify consolidation is signed by the source and the target + if (!verifyConsolidationSignature(state, signedConsolidation)) { + throw new Error("Consolidation not valid"); + } +} diff --git a/packages/state-transition/src/block/processDeposit.ts b/packages/state-transition/src/block/processDeposit.ts index fbefafdc7fbf..676d78e27578 100644 --- a/packages/state-transition/src/block/processDeposit.ts +++ b/packages/state-transition/src/block/processDeposit.ts @@ -14,9 +14,17 @@ import { import {DepositData} from "@lodestar/types/lib/phase0/types.js"; import {DepositReceipt} from "@lodestar/types/lib/electra/types.js"; +import {BeaconConfig} from "@lodestar/config"; import {ZERO_HASH} from "../constants/index.js"; -import {computeDomain, computeSigningRoot, increaseBalance} from "../util/index.js"; -import {CachedBeaconStateAllForks, CachedBeaconStateAltair} from "../types.js"; +import { + computeDomain, + computeSigningRoot, + hasCompoundingWithdrawalCredential, + hasEth1WithdrawalCredential, + increaseBalance, + switchToCompoundingValidator, +} from "../util/index.js"; +import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStateElectra} from "../types.js"; /** * Process a Deposit operation. Potentially adds a new validator to the registry. Mutates the validators and balances @@ -59,29 +67,29 @@ export function applyDeposit( const cachedIndex = epochCtx.getValidatorIndex(pubkey); if (cachedIndex === undefined || !Number.isSafeInteger(cachedIndex) || cachedIndex >= validators.length) { - // verify the deposit signature (proof of posession) which is not checked by the deposit contract - const depositMessage = { - pubkey, - withdrawalCredentials, - amount, - }; - // fork-agnostic domain since deposits are valid across forks - const domain = computeDomain(DOMAIN_DEPOSIT, config.GENESIS_FORK_VERSION, ZERO_HASH); - const signingRoot = computeSigningRoot(ssz.phase0.DepositMessage, depositMessage, domain); - try { - // Pubkeys must be checked for group + inf. This must be done only once when the validator deposit is processed - const publicKey = bls.PublicKey.fromBytes(pubkey, CoordType.affine, true); - const signature = bls.Signature.fromBytes(deposit.signature, CoordType.affine, true); - if (!signature.verify(publicKey, signingRoot)) { - return; - } - } catch (e) { - return; // Catch all BLS errors: failed key validation, failed signature validation, invalid signature + if (isValidDepositSignature(config, pubkey, withdrawalCredentials, amount, deposit.signature)) { + addValidatorToRegistry(fork, state, pubkey, withdrawalCredentials, amount); } - addValidatorToRegistry(fork, state, pubkey, withdrawalCredentials, amount); } else { - // increase balance by deposit amount - increaseBalance(state, cachedIndex, amount); + if (fork < ForkSeq.electra) { + // increase balance by deposit amount right away pre-electra + increaseBalance(state, cachedIndex, amount); + } else if (fork >= ForkSeq.electra) { + const stateElectra = state as CachedBeaconStateElectra; + const pendingBalanceDeposit = ssz.electra.PendingBalanceDeposit.toViewDU({ + index: cachedIndex, + amount: BigInt(amount), + }); + stateElectra.pendingBalanceDeposits.push(pendingBalanceDeposit); + + if ( + hasCompoundingWithdrawalCredential(withdrawalCredentials) && + hasEth1WithdrawalCredential(validators.getReadonly(cachedIndex).withdrawalCredentials) && + isValidDepositSignature(config, pubkey, withdrawalCredentials, amount, deposit.signature) + ) { + switchToCompoundingValidator(stateElectra, cachedIndex); + } + } } } @@ -94,7 +102,8 @@ function addValidatorToRegistry( ): void { const {validators, epochCtx} = state; // add validator and balance entries - const effectiveBalance = Math.min(amount - (amount % EFFECTIVE_BALANCE_INCREMENT), MAX_EFFECTIVE_BALANCE); + const effectiveBalance = + fork < ForkSeq.electra ? Math.min(amount - (amount % EFFECTIVE_BALANCE_INCREMENT), MAX_EFFECTIVE_BALANCE) : 0; validators.push( ssz.phase0.Validator.toViewDU({ pubkey, @@ -107,9 +116,9 @@ function addValidatorToRegistry( slashed: false, }) ); - state.balances.push(amount); const validatorIndex = validators.length - 1; + // TODO Electra: Review this // Updating here is better than updating at once on epoch transition // - Simplify genesis fn applyDeposits(): effectiveBalanceIncrements is populated immediately // - Keep related code together to reduce risk of breaking this cache @@ -129,4 +138,43 @@ function addValidatorToRegistry( stateAltair.previousEpochParticipation.push(0); stateAltair.currentEpochParticipation.push(0); } + + if (fork < ForkSeq.electra) { + state.balances.push(amount); + } else if (fork >= ForkSeq.electra) { + state.balances.push(0); + const stateElectra = state as CachedBeaconStateElectra; + const pendingBalanceDeposit = ssz.electra.PendingBalanceDeposit.toViewDU({ + index: validatorIndex, + amount: BigInt(amount), + }); + stateElectra.pendingBalanceDeposits.push(pendingBalanceDeposit); + } +} + +function isValidDepositSignature( + config: BeaconConfig, + pubkey: Uint8Array, + withdrawalCredentials: Uint8Array, + amount: number, + depositSignature: Uint8Array +): boolean { + // verify the deposit signature (proof of posession) which is not checked by the deposit contract + const depositMessage = { + pubkey, + withdrawalCredentials, + amount, + }; + // fork-agnostic domain since deposits are valid across forks + const domain = computeDomain(DOMAIN_DEPOSIT, config.GENESIS_FORK_VERSION, ZERO_HASH); + const signingRoot = computeSigningRoot(ssz.phase0.DepositMessage, depositMessage, domain); + try { + // Pubkeys must be checked for group + inf. This must be done only once when the validator deposit is processed + const publicKey = bls.PublicKey.fromBytes(pubkey, CoordType.affine, true); + const signature = bls.Signature.fromBytes(depositSignature, CoordType.affine, true); + + return signature.verify(publicKey, signingRoot); + } catch (e) { + return false; // Catch all BLS errors: failed key validation, failed signature validation, invalid signature + } } diff --git a/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts b/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts index cfe719bf57f3..e16fad105aaa 100644 --- a/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts +++ b/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts @@ -1,67 +1,99 @@ -import {CompositeViewDU} from "@chainsafe/ssz"; -import {electra, ssz} from "@lodestar/types"; -import {ETH1_ADDRESS_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH} from "@lodestar/params"; +import {toHexString} from "@chainsafe/ssz"; +import {electra, phase0, ssz} from "@lodestar/types"; +import { + FAR_FUTURE_EPOCH, + MIN_ACTIVATION_BALANCE, + PENDING_PARTIAL_WITHDRAWALS_LIMIT, + FULL_EXIT_REQUEST_AMOUNT, + ForkSeq, +} from "@lodestar/params"; -import {isActiveValidator} from "../util/index.js"; import {CachedBeaconStateElectra} from "../types.js"; -import {initiateValidatorExit} from "./index.js"; +import {hasCompoundingWithdrawalCredential, hasExecutionWithdrawalCredential} from "../util/electra.js"; +import {getPendingBalanceToWithdraw, isActiveValidator} from "../util/validator.js"; +import {computeExitEpochAndUpdateChurn} from "../util/epoch.js"; +import {initiateValidatorExit} from "./initiateValidatorExit.js"; -const FULL_EXIT_REQUEST_AMOUNT = 0; -/** - * Process execution layer exit messages and initiate exit incase they belong to a valid active validator - * otherwise silent ignore. - */ export function processExecutionLayerWithdrawalRequest( + fork: ForkSeq, state: CachedBeaconStateElectra, - withdrawalRequest: electra.ExecutionLayerWithdrawalRequest + executionLayerWithdrawalRequest: electra.ExecutionLayerWithdrawalRequest ): void { - const isFullExitRequest = withdrawalRequest.amount === FULL_EXIT_REQUEST_AMOUNT; + const amount = Number(executionLayerWithdrawalRequest.amount); + const {pendingPartialWithdrawals, validators, epochCtx} = state; + // no need to use unfinalized pubkey cache from 6110 as validator won't be active anyway + const {pubkey2index, config} = epochCtx; + const isFullExitRequest = amount === FULL_EXIT_REQUEST_AMOUNT; - if (isFullExitRequest) { - const validator = isValidExecutionLayerExit(state, withdrawalRequest); - if (validator === null) { - return; - } - - initiateValidatorExit(state, validator); - } else { - // partial withdral request add codeblock + // If partial withdrawal queue is full, only full exits are processed + if (pendingPartialWithdrawals.length >= PENDING_PARTIAL_WITHDRAWALS_LIMIT && !isFullExitRequest) { + return; } -} -// TODO electra : add pending withdrawal check before exit -export function isValidExecutionLayerExit( - state: CachedBeaconStateElectra, - exit: electra.ExecutionLayerWithdrawalRequest -): 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; + // bail out if validator is not in beacon state + // note that we don't need to check for 6110 unfinalized vals as they won't be eligible for withdraw/exit anyway + const validatorIndex = pubkey2index.get(executionLayerWithdrawalRequest.validatorPubkey); + if (validatorIndex === undefined) { + return; } - const {withdrawalCredentials} = validator; - if (withdrawalCredentials[0] !== ETH1_ADDRESS_WITHDRAWAL_PREFIX) { - return null; + const validator = validators.getReadonly(validatorIndex); + if (!isValidatorEligibleForWithdrawOrExit(validator, executionLayerWithdrawalRequest.sourceAddress, state)) { + return; } - const executionAddress = withdrawalCredentials.subarray(12, 32); - if (Buffer.compare(executionAddress, exit.sourceAddress) !== 0) { - return null; + // TODO Electra: Consider caching pendingPartialWithdrawals + const pendingBalanceToWithdraw = getPendingBalanceToWithdraw(state, validatorIndex); + const validatorBalance = state.balances.get(validatorIndex); + + if (isFullExitRequest) { + // only exit validator if it has no pending withdrawals in the queue + if (pendingBalanceToWithdraw === 0) { + initiateValidatorExit(fork, state, validator); + } + return; } - const currentEpoch = epochCtx.epoch; + // partial withdrawal request + const hasSufficientEffectiveBalance = validator.effectiveBalance >= MIN_ACTIVATION_BALANCE; + const hasExcessBalance = validatorBalance > MIN_ACTIVATION_BALANCE + pendingBalanceToWithdraw; + + // Only allow partial withdrawals with compounding withdrawal credentials if ( - // verify the validator is active + hasCompoundingWithdrawalCredential(validator.withdrawalCredentials) && + hasSufficientEffectiveBalance && + hasExcessBalance + ) { + const amountToWithdraw = BigInt( + Math.min(validatorBalance - MIN_ACTIVATION_BALANCE - pendingBalanceToWithdraw, amount) + ); + const exitQueueEpoch = computeExitEpochAndUpdateChurn(state, amountToWithdraw); + const withdrawableEpoch = exitQueueEpoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY; + + const pendingPartialWithdrawal = ssz.electra.PartialWithdrawal.toViewDU({ + index: validatorIndex, + amount: amountToWithdraw, + withdrawableEpoch, + }); + state.pendingPartialWithdrawals.push(pendingPartialWithdrawal); + } +} + +function isValidatorEligibleForWithdrawOrExit( + validator: phase0.Validator, + sourceAddress: Uint8Array, + state: CachedBeaconStateElectra +): boolean { + const {withdrawalCredentials} = validator; + const addressStr = toHexString(withdrawalCredentials.subarray(12)); + const sourceAddressStr = toHexString(sourceAddress); + const {epoch: currentEpoch, config} = state.epochCtx; + + return ( + hasExecutionWithdrawalCredential(withdrawalCredentials) && + addressStr === sourceAddressStr && 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 b4c8975f23bd..5c8e6086b3c6 100644 --- a/packages/state-transition/src/block/processOperations.ts +++ b/packages/state-transition/src/block/processOperations.ts @@ -8,10 +8,11 @@ import {processProposerSlashing} from "./processProposerSlashing.js"; import {processAttesterSlashing} from "./processAttesterSlashing.js"; import {processDeposit} from "./processDeposit.js"; import {processVoluntaryExit} from "./processVoluntaryExit.js"; -import {processExecutionLayerWithdrawalRequest} from "./processExecutionLayerWithdrawalRequest.js"; import {processBlsToExecutionChange} from "./processBlsToExecutionChange.js"; +import {processExecutionLayerWithdrawalRequest} from "./processExecutionLayerWithdrawalRequest.js"; import {processDepositReceipt} from "./processDepositReceipt.js"; import {ProcessBlockOpts} from "./types.js"; +import {processConsolidation} from "./processConsolidation.js"; export { processProposerSlashing, @@ -52,12 +53,7 @@ export function processOperations( } for (const voluntaryExit of body.voluntaryExits) { - processVoluntaryExit(state, voluntaryExit, opts.verifySignatures); - } - if (fork >= ForkSeq.electra) { - for (const elWithdrawalRequest of (body as electra.BeaconBlockBody).executionPayload.withdrawalRequests) { - processExecutionLayerWithdrawalRequest(state as CachedBeaconStateElectra, elWithdrawalRequest); - } + processVoluntaryExit(fork, state, voluntaryExit, opts.verifySignatures); } if (fork >= ForkSeq.capella) { @@ -67,8 +63,19 @@ export function processOperations( } if (fork >= ForkSeq.electra) { - for (const depositReceipt of (body as electra.BeaconBlockBody).executionPayload.depositReceipts) { - processDepositReceipt(fork, state as CachedBeaconStateElectra, depositReceipt); + const stateElectra = state as CachedBeaconStateElectra; + const bodyElectra = body as electra.BeaconBlockBody; + + for (const elWithdrawalRequest of bodyElectra.executionPayload.withdrawalRequests) { + processExecutionLayerWithdrawalRequest(fork, state as CachedBeaconStateElectra, elWithdrawalRequest); + } + + for (const depositReceipt of bodyElectra.executionPayload.depositReceipts) { + processDepositReceipt(fork, stateElectra, depositReceipt); + } + + for (const consolidation of bodyElectra.consolidations) { + processConsolidation(stateElectra, consolidation); } } } diff --git a/packages/state-transition/src/block/processVoluntaryExit.ts b/packages/state-transition/src/block/processVoluntaryExit.ts index 80982623a447..b08aa7800884 100644 --- a/packages/state-transition/src/block/processVoluntaryExit.ts +++ b/packages/state-transition/src/block/processVoluntaryExit.ts @@ -1,7 +1,7 @@ -import {FAR_FUTURE_EPOCH} from "@lodestar/params"; +import {FAR_FUTURE_EPOCH, ForkSeq} from "@lodestar/params"; import {phase0} from "@lodestar/types"; -import {isActiveValidator} from "../util/index.js"; -import {CachedBeaconStateAllForks} from "../types.js"; +import {getPendingBalanceToWithdraw, isActiveValidator} from "../util/index.js"; +import {CachedBeaconStateAllForks, CachedBeaconStateElectra} from "../types.js"; import {verifyVoluntaryExitSignature} from "../signatureSets/index.js"; import {initiateValidatorExit} from "./index.js"; @@ -11,16 +11,21 @@ import {initiateValidatorExit} from "./index.js"; * PERF: Work depends on number of VoluntaryExit per block. On regular networks the average is 0 / block. */ export function processVoluntaryExit( + fork: ForkSeq, state: CachedBeaconStateAllForks, signedVoluntaryExit: phase0.SignedVoluntaryExit, verifySignature = true ): void { - if (!isValidVoluntaryExit(state, signedVoluntaryExit, verifySignature)) { - throw Error("Invalid voluntary exit"); + const isValidExit = + fork >= ForkSeq.electra + ? isValidVoluntaryExitElectra(state as CachedBeaconStateElectra, signedVoluntaryExit, verifySignature) + : isValidVoluntaryExit(state, signedVoluntaryExit, verifySignature); + if (!isValidExit) { + throw Error(`Invalid voluntary exit at forkSeq=${fork}`); } const validator = state.validators.get(signedVoluntaryExit.message.validatorIndex); - initiateValidatorExit(state, validator); + initiateValidatorExit(fork, state, validator); } export function isValidVoluntaryExit( @@ -46,3 +51,16 @@ export function isValidVoluntaryExit( (!verifySignature || verifyVoluntaryExitSignature(state, signedVoluntaryExit)) ); } + +function isValidVoluntaryExitElectra( + state: CachedBeaconStateElectra, + signedVoluntaryExit: phase0.SignedVoluntaryExit, + verifySignature = true +): boolean { + // only exit validator if it has no pending withdrawals in the queue (post-Electra only) + if (getPendingBalanceToWithdraw(state, signedVoluntaryExit.message.validatorIndex) === 0) { + return isValidVoluntaryExit(state, signedVoluntaryExit, verifySignature); + } + + return false; +} diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index ddea73c27a26..e81c973e4558 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -1,19 +1,29 @@ import {byteArrayEquals, toHexString} from "@chainsafe/ssz"; import {ssz, capella} from "@lodestar/types"; import { - MAX_EFFECTIVE_BALANCE, MAX_WITHDRAWALS_PER_PAYLOAD, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP, + ForkSeq, + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP, + FAR_FUTURE_EPOCH, + MIN_ACTIVATION_BALANCE, } from "@lodestar/params"; -import {CachedBeaconStateCapella} from "../types.js"; -import {decreaseBalance, hasEth1WithdrawalCredential, isCapellaPayloadHeader} from "../util/index.js"; +import {CachedBeaconStateCapella, CachedBeaconStateElectra} from "../types.js"; +import { + decreaseBalance, + getValidatorMaxEffectiveBalance, + isCapellaPayloadHeader, + isFullyWithdrawableValidator, + isPartiallyWithdrawableValidator, +} from "../util/index.js"; export function processWithdrawals( - state: CachedBeaconStateCapella, + fork: ForkSeq, + state: CachedBeaconStateCapella | CachedBeaconStateElectra, payload: capella.FullOrBlindedExecutionPayload ): void { - const {withdrawals: expectedWithdrawals} = getExpectedWithdrawals(state); + const {withdrawals: expectedWithdrawals, partialWithdrawalsCount} = getExpectedWithdrawals(fork, state); const numWithdrawals = expectedWithdrawals.length; if (isCapellaPayloadHeader(payload)) { @@ -43,6 +53,11 @@ export function processWithdrawals( decreaseBalance(state, withdrawal.validatorIndex, Number(withdrawal.amount)); } + if (fork >= ForkSeq.electra) { + const stateElectra = state as CachedBeaconStateElectra; + stateElectra.pendingPartialWithdrawals = stateElectra.pendingPartialWithdrawals.sliceFrom(partialWithdrawalsCount); + } + // Update the nextWithdrawalIndex if (expectedWithdrawals.length > 0) { const latestWithdrawal = expectedWithdrawals[expectedWithdrawals.length - 1]; @@ -62,46 +77,80 @@ export function processWithdrawals( } } -export function getExpectedWithdrawals(state: CachedBeaconStateCapella): { +export function getExpectedWithdrawals( + fork: ForkSeq, + state: CachedBeaconStateCapella | CachedBeaconStateElectra +): { withdrawals: capella.Withdrawal[]; sampledValidators: number; + partialWithdrawalsCount: number; } { const epoch = state.epochCtx.epoch; let withdrawalIndex = state.nextWithdrawalIndex; const {validators, balances, nextWithdrawalValidatorIndex} = state; - const bound = Math.min(validators.length, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP); - - let n = 0; const withdrawals: capella.Withdrawal[] = []; + + if (fork >= ForkSeq.electra) { + const stateElectra = state as CachedBeaconStateElectra; + + for (const withdrawal of stateElectra.pendingPartialWithdrawals.getAllReadonly()) { + if (withdrawal.withdrawableEpoch > epoch || withdrawals.length === MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP) { + break; + } + + const validator = validators.getReadonly(withdrawal.index); + + if ( + validator.exitEpoch === FAR_FUTURE_EPOCH && + validator.effectiveBalance >= MIN_ACTIVATION_BALANCE && + balances.get(withdrawalIndex) > MIN_ACTIVATION_BALANCE + ) { + const balanceOverMinActivationBalance = BigInt(balances.get(withdrawalIndex) - MIN_ACTIVATION_BALANCE); + const withdrawableBalance = + balanceOverMinActivationBalance < withdrawal.amount ? balanceOverMinActivationBalance : withdrawal.amount; + withdrawals.push({ + index: withdrawalIndex, + validatorIndex: withdrawal.index, + address: validator.withdrawalCredentials.subarray(12), + amount: withdrawableBalance, + }); + withdrawalIndex++; + } + } + } + + const partialWithdrawalsCount = withdrawals.length; + const bound = Math.min(validators.length, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP); + let n = 0; // Just run a bounded loop max iterating over all withdrawals // however breaks out once we have MAX_WITHDRAWALS_PER_PAYLOAD for (n = 0; n < bound; n++) { // Get next validator in turn const validatorIndex = (nextWithdrawalValidatorIndex + n) % validators.length; - // It's most likely for validators to not have set eth1 credentials, than having 0 balance const validator = validators.getReadonly(validatorIndex); - if (!hasEth1WithdrawalCredential(validator.withdrawalCredentials)) { + const balance = balances.get(validatorIndex); + // early skip for balance = 0 as its now more likely that validator has exited/slahed with + // balance zero than not have withdrawal credentials set + if (balance === 0) { continue; } - const balance = balances.get(validatorIndex); - - if (balance > 0 && validator.withdrawableEpoch <= epoch) { + if (isFullyWithdrawableValidator(fork, validator, balance, epoch)) { withdrawals.push({ index: withdrawalIndex, validatorIndex, - address: validator.withdrawalCredentials.slice(12), + address: validator.withdrawalCredentials.subarray(12), amount: BigInt(balance), }); withdrawalIndex++; - } else if (validator.effectiveBalance === MAX_EFFECTIVE_BALANCE && balance > MAX_EFFECTIVE_BALANCE) { + } else if (isPartiallyWithdrawableValidator(fork, validator, balance)) { withdrawals.push({ index: withdrawalIndex, validatorIndex, - address: validator.withdrawalCredentials.slice(12), - amount: BigInt(balance - MAX_EFFECTIVE_BALANCE), + address: validator.withdrawalCredentials.subarray(12), + amount: BigInt(balance - getValidatorMaxEffectiveBalance(validator.withdrawalCredentials)), }); withdrawalIndex++; } @@ -112,5 +161,5 @@ export function getExpectedWithdrawals(state: CachedBeaconStateCapella): { } } - return {withdrawals, sampledValidators: n}; + return {withdrawals, sampledValidators: n, partialWithdrawalsCount}; } diff --git a/packages/state-transition/src/block/slashValidator.ts b/packages/state-transition/src/block/slashValidator.ts index 9f3eb2947644..c4b7d5f848ea 100644 --- a/packages/state-transition/src/block/slashValidator.ts +++ b/packages/state-transition/src/block/slashValidator.ts @@ -6,11 +6,13 @@ import { MIN_SLASHING_PENALTY_QUOTIENT, MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR, MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX, + MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA, PROPOSER_REWARD_QUOTIENT, PROPOSER_WEIGHT, TIMELY_TARGET_FLAG_INDEX, WEIGHT_DENOMINATOR, WHISTLEBLOWER_REWARD_QUOTIENT, + WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA, } from "@lodestar/params"; import {decreaseBalance, increaseBalance} from "../util/index.js"; @@ -31,7 +33,7 @@ export function slashValidator( const validator = state.validators.get(slashedIndex); // TODO: Bellatrix initiateValidatorExit validators.update() with the one below - initiateValidatorExit(state, validator); + initiateValidatorExit(fork, state, validator); validator.slashed = true; validator.withdrawableEpoch = Math.max(validator.withdrawableEpoch, epoch + EPOCHS_PER_SLASHINGS_VECTOR); @@ -41,7 +43,7 @@ export function slashValidator( // state.slashings is initially a Gwei (BigInt) vector, however since Nov 2023 it's converted to UintNum64 (number) vector in the state transition because: // - state.slashings[nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR] is reset per epoch in processSlashingsReset() // - max slashed validators per epoch is SLOTS_PER_EPOCH * MAX_ATTESTER_SLASHINGS * MAX_VALIDATORS_PER_COMMITTEE which is 32 * 2 * 2048 = 131072 on mainnet - // - with that and 32_000_000_000 MAX_EFFECTIVE_BALANCE, it still fits in a number given that Math.floor(Number.MAX_SAFE_INTEGER / 32_000_000_000) = 281474 + // - with that and 32_000_000_000 MAX_EFFECTIVE_BALANCE or 2048_000_000_000 MAX_EFFECTIVE_BALANCE_ELECTRA, it still fits in a number given that Math.floor(Number.MAX_SAFE_INTEGER / 32_000_000_000) = 281474 // - we don't need to compute the total slashings from state.slashings, it's handled by totalSlashingsByIncrement in EpochCache const slashingIndex = epoch % EPOCHS_PER_SLASHINGS_VECTOR; state.slashings.set(slashingIndex, (state.slashings.get(slashingIndex) ?? 0) + effectiveBalance); @@ -52,11 +54,16 @@ export function slashValidator( ? MIN_SLASHING_PENALTY_QUOTIENT : fork === ForkSeq.altair ? MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR - : MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX; + : fork < ForkSeq.electra // no change from bellatrix to deneb + ? MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX + : MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA; decreaseBalance(state, slashedIndex, Math.floor(effectiveBalance / minSlashingPenaltyQuotient)); // apply proposer and whistleblower rewards - const whistleblowerReward = Math.floor(effectiveBalance / WHISTLEBLOWER_REWARD_QUOTIENT); + const whistleblowerReward = + fork < ForkSeq.electra + ? Math.floor(effectiveBalance / WHISTLEBLOWER_REWARD_QUOTIENT) + : Math.floor(effectiveBalance / WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA); const proposerReward = fork === ForkSeq.phase0 ? Math.floor(whistleblowerReward / PROPOSER_REWARD_QUOTIENT) diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index ade519719fc0..804f606ab76a 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -181,6 +181,7 @@ export class EpochCache { * initiateValidatorExit(). This value may vary on each fork of the state. * * NOTE: Changes block to block + * NOTE: No longer used by initiateValidatorExit post-electra */ exitQueueEpoch: Epoch; /** @@ -188,6 +189,7 @@ export class EpochCache { * initiateValidatorExit(). This value may vary on each fork of the state. * * NOTE: Changes block to block + * NOTE: No longer used by initiateValidatorExit post-electra */ exitQueueChurn: number; diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index dc4edf26e084..42e8ccc3b790 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -1,6 +1,6 @@ import {Epoch, ValidatorIndex} from "@lodestar/types"; import {intDiv} from "@lodestar/utils"; -import {EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, ForkSeq, MAX_EFFECTIVE_BALANCE} from "@lodestar/params"; +import {EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, ForkSeq, MIN_ACTIVATION_BALANCE} from "@lodestar/params"; import { AttesterStatus, @@ -89,7 +89,7 @@ export interface EpochTransitionCache { /** * Indices of validators that just joined and will be eligible for the active queue. * ``` - * v.activationEligibilityEpoch === FAR_FUTURE_EPOCH && v.effectiveBalance === MAX_EFFECTIVE_BALANCE + * v.activationEligibilityEpoch === FAR_FUTURE_EPOCH && v.effectiveBalance >= MAX_EFFECTIVE_BALANCE * ``` * All validators in indicesEligibleForActivationQueue get activationEligibilityEpoch set. So it can only include * validators that have just joined the registry through a valid full deposit(s). @@ -243,12 +243,12 @@ export function beforeProcessEpoch( // def is_eligible_for_activation_queue(validator: Validator) -> bool: // return ( // validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH - // and validator.effective_balance == MAX_EFFECTIVE_BALANCE + // and validator.effective_balance >= MAX_EFFECTIVE_BALANCE # [Modified in Electra] // ) // ``` if ( validator.activationEligibilityEpoch === FAR_FUTURE_EPOCH && - validator.effectiveBalance === MAX_EFFECTIVE_BALANCE + validator.effectiveBalance >= MIN_ACTIVATION_BALANCE ) { indicesEligibleForActivationQueue.push(i); } diff --git a/packages/state-transition/src/epoch/index.ts b/packages/state-transition/src/epoch/index.ts index b55ebe291fb9..6e736fdae2cc 100644 --- a/packages/state-transition/src/epoch/index.ts +++ b/packages/state-transition/src/epoch/index.ts @@ -11,6 +11,7 @@ import { CachedBeaconStateAltair, CachedBeaconStatePhase0, EpochTransitionCache, + CachedBeaconStateElectra, } from "../types.js"; import {BeaconStateTransitionMetrics} from "../metrics.js"; import {processEffectiveBalanceUpdates} from "./processEffectiveBalanceUpdates.js"; @@ -27,6 +28,8 @@ import {processRewardsAndPenalties} from "./processRewardsAndPenalties.js"; import {processSlashings} from "./processSlashings.js"; import {processSlashingsReset} from "./processSlashingsReset.js"; import {processSyncCommitteeUpdates} from "./processSyncCommitteeUpdates.js"; +import {processPendingBalanceDeposits} from "./processPendingBalanceDeposits.js"; +import {processPendingConsolidations} from "./processPendingConsolidations.js"; // For spec tests export {getRewardsAndPenalties} from "./processRewardsAndPenalties.js"; @@ -45,6 +48,8 @@ export { processParticipationFlagUpdates, processSyncCommitteeUpdates, processHistoricalSummariesUpdate, + processPendingBalanceDeposits, + processPendingConsolidations, }; export {computeUnrealizedCheckpoints} from "./computeUnrealizedCheckpoints.js"; @@ -65,6 +70,8 @@ export enum EpochTransitionStep { processEffectiveBalanceUpdates = "processEffectiveBalanceUpdates", processParticipationFlagUpdates = "processParticipationFlagUpdates", processSyncCommitteeUpdates = "processSyncCommitteeUpdates", + processPendingBalanceDeposits = "processPendingBalanceDeposits", + processPendingConsolidations = "processPendingConsolidations", } export function processEpoch( @@ -76,7 +83,7 @@ export function processEpoch( // state.slashings is initially a Gwei (BigInt) vector, however since Nov 2023 it's converted to UintNum64 (number) vector in the state transition because: // - state.slashings[nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR] is reset per epoch in processSlashingsReset() // - max slashed validators per epoch is SLOTS_PER_EPOCH * MAX_ATTESTER_SLASHINGS * MAX_VALIDATORS_PER_COMMITTEE which is 32 * 2 * 2048 = 131072 on mainnet - // - with that and 32_000_000_000 MAX_EFFECTIVE_BALANCE, it still fits in a number given that Math.floor(Number.MAX_SAFE_INTEGER / 32_000_000_000) = 281474 + // - with that and 32_000_000_000 MAX_EFFECTIVE_BALANCE or 2048_000_000_000 MAX_EFFECTIVE_BALANCE_ELECTRA, it still fits in a number given that Math.floor(Number.MAX_SAFE_INTEGER / 32_000_000_000) = 281474 if (maxValidatorsPerStateSlashing > maxSafeValidators) { throw new Error("Lodestar does not support this network, parameters don't fit number value inside state.slashings"); } @@ -100,7 +107,7 @@ export function processEpoch( // processRewardsAndPenalties(state, cache); { const timer = metrics?.epochTransitionStepTime.startTimer({step: EpochTransitionStep.processRegistryUpdates}); - processRegistryUpdates(state, cache); + processRegistryUpdates(fork, state, cache); timer?.(); } @@ -120,11 +127,30 @@ export function processEpoch( processEth1DataReset(state, cache); + if (fork >= ForkSeq.electra) { + const stateElectra = state as CachedBeaconStateElectra; + { + const timer = metrics?.epochTransitionStepTime.startTimer({ + step: EpochTransitionStep.processPendingBalanceDeposits, + }); + processPendingBalanceDeposits(stateElectra); + timer?.(); + } + + { + const timer = metrics?.epochTransitionStepTime.startTimer({ + step: EpochTransitionStep.processPendingConsolidations, + }); + processPendingConsolidations(stateElectra); + timer?.(); + } + } + { const timer = metrics?.epochTransitionStepTime.startTimer({ step: EpochTransitionStep.processEffectiveBalanceUpdates, }); - processEffectiveBalanceUpdates(state, cache); + processEffectiveBalanceUpdates(fork, state, cache); timer?.(); } diff --git a/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts b/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts index 5f1df35b7215..e83d2545ef50 100644 --- a/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts +++ b/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts @@ -5,9 +5,12 @@ import { HYSTERESIS_QUOTIENT, HYSTERESIS_UPWARD_MULTIPLIER, MAX_EFFECTIVE_BALANCE, + MAX_EFFECTIVE_BALANCE_ELECTRA, + MIN_ACTIVATION_BALANCE, TIMELY_TARGET_FLAG_INDEX, } from "@lodestar/params"; import {EpochTransitionCache, CachedBeaconStateAllForks, BeaconStateAltair} from "../types.js"; +import {hasCompoundingWithdrawalCredential} from "../util/electra.js"; /** Same to https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.5/specs/altair/beacon-chain.md#has_flag */ const TIMELY_TARGET = 1 << TIMELY_TARGET_FLAG_INDEX; @@ -21,7 +24,11 @@ const TIMELY_TARGET = 1 << TIMELY_TARGET_FLAG_INDEX; * - On normal mainnet conditions 0 validators change their effective balance * - In case of big innactivity event a medium portion of validators may have their effectiveBalance updated */ -export function processEffectiveBalanceUpdates(state: CachedBeaconStateAllForks, cache: EpochTransitionCache): void { +export function processEffectiveBalanceUpdates( + fork: ForkSeq, + state: CachedBeaconStateAllForks, + cache: EpochTransitionCache +): void { const HYSTERESIS_INCREMENT = EFFECTIVE_BALANCE_INCREMENT / HYSTERESIS_QUOTIENT; const DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER; const UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER; @@ -42,6 +49,7 @@ export function processEffectiveBalanceUpdates(state: CachedBeaconStateAllForks, // PERF: It's faster to access to get() every single element (4ms) than to convert to regular array then loop (9ms) let effectiveBalanceIncrement = effectiveBalanceIncrements[i]; let effectiveBalance = effectiveBalanceIncrement * EFFECTIVE_BALANCE_INCREMENT; + let effectiveBalanceLimit; if ( // Too big @@ -49,10 +57,20 @@ export function processEffectiveBalanceUpdates(state: CachedBeaconStateAllForks, // Too small. Check effectiveBalance < MAX_EFFECTIVE_BALANCE to prevent unnecessary updates (effectiveBalance < MAX_EFFECTIVE_BALANCE && effectiveBalance < balance - UPWARD_THRESHOLD) ) { - effectiveBalance = Math.min(balance - (balance % EFFECTIVE_BALANCE_INCREMENT), MAX_EFFECTIVE_BALANCE); // Update the state tree // Should happen rarely, so it's fine to update the tree const validator = validators.get(i); + + if (fork < ForkSeq.electra) { + effectiveBalanceLimit = MAX_EFFECTIVE_BALANCE; + } else { + // Electra or after + effectiveBalanceLimit = hasCompoundingWithdrawalCredential(validator.withdrawalCredentials) + ? MAX_EFFECTIVE_BALANCE_ELECTRA + : MIN_ACTIVATION_BALANCE; + } + + effectiveBalance = Math.min(balance - (balance % EFFECTIVE_BALANCE_INCREMENT), effectiveBalanceLimit); validator.effectiveBalance = effectiveBalance; // Also update the fast cached version const newEffectiveBalanceIncrement = Math.floor(effectiveBalance / EFFECTIVE_BALANCE_INCREMENT); diff --git a/packages/state-transition/src/epoch/processPendingBalanceDeposits.ts b/packages/state-transition/src/epoch/processPendingBalanceDeposits.ts new file mode 100644 index 000000000000..95928d66df2d --- /dev/null +++ b/packages/state-transition/src/epoch/processPendingBalanceDeposits.ts @@ -0,0 +1,35 @@ +import {CachedBeaconStateElectra} from "../types.js"; +import {increaseBalance} from "../util/balance.js"; +import {getActivationExitChurnLimit} from "../util/validator.js"; + +/** + * Starting from Electra: + * Process pending balance deposits from state subject to churn limit and depsoitBalanceToConsume. + * For each eligible `deposit`, call `increaseBalance()`. + * Remove the processed deposits from `state.pendingBalanceDeposits`. + * Update `state.depositBalanceToConsume` for the next epoch + */ +export function processPendingBalanceDeposits(state: CachedBeaconStateElectra): void { + const availableForProcessing = state.depositBalanceToConsume + BigInt(getActivationExitChurnLimit(state)); + let processedAmount = 0n; + let nextDepositIndex = 0; + + for (const deposit of state.pendingBalanceDeposits.getAllReadonly()) { + const {amount} = deposit; + if (processedAmount + amount > availableForProcessing) { + break; + } + increaseBalance(state, deposit.index, Number(amount)); + processedAmount = processedAmount + amount; + nextDepositIndex++; + } + + const remainingPendingBalanceDeposits = state.pendingBalanceDeposits.sliceFrom(nextDepositIndex); + state.pendingBalanceDeposits = remainingPendingBalanceDeposits; + + if (remainingPendingBalanceDeposits.length === 0) { + state.depositBalanceToConsume = 0n; + } else { + state.depositBalanceToConsume = availableForProcessing - processedAmount; + } +} diff --git a/packages/state-transition/src/epoch/processPendingConsolidations.ts b/packages/state-transition/src/epoch/processPendingConsolidations.ts new file mode 100644 index 000000000000..e025a454f64e --- /dev/null +++ b/packages/state-transition/src/epoch/processPendingConsolidations.ts @@ -0,0 +1,45 @@ +import {CachedBeaconStateElectra} from "../types.js"; +import {decreaseBalance, increaseBalance} from "../util/balance.js"; +import {getActiveBalance} from "../util/validator.js"; +import {switchToCompoundingValidator} from "../util/electra.js"; + +/** + * Starting from Electra: + * Process every `pendingConsolidation` in `state.pendingConsolidations`. + * Churn limit was applied when enqueueing so we don't care about the limit here + * However we only process consolidations up to current epoch + * + * For each valid `pendingConsolidation`, update withdrawal credential of target + * validator to compounding, decrease balance of source validator and increase balance + * of target validator. + * + * Dequeue all processed consolidations from `state.pendingConsolidation` + * + */ +export function processPendingConsolidations(state: CachedBeaconStateElectra): void { + let nextPendingConsolidation = 0; + + for (const pendingConsolidation of state.pendingConsolidations.getAllReadonly()) { + const {sourceIndex, targetIndex} = pendingConsolidation; + const sourceValidator = state.validators.getReadonly(sourceIndex); + + if (sourceValidator.slashed) { + nextPendingConsolidation++; + continue; + } + + if (sourceValidator.withdrawableEpoch > state.epochCtx.epoch) { + break; + } + // Churn any target excess active balance of target and raise its max + switchToCompoundingValidator(state, targetIndex); + // Move active balance to target. Excess balance is withdrawable. + const activeBalance = getActiveBalance(state, sourceIndex); + decreaseBalance(state, sourceIndex, activeBalance); + increaseBalance(state, targetIndex, activeBalance); + + nextPendingConsolidation++; + } + + state.pendingConsolidations = state.pendingConsolidations.sliceFrom(nextPendingConsolidation); +} diff --git a/packages/state-transition/src/epoch/processRegistryUpdates.ts b/packages/state-transition/src/epoch/processRegistryUpdates.ts index 0591f982d1d5..3804b273cd8b 100644 --- a/packages/state-transition/src/epoch/processRegistryUpdates.ts +++ b/packages/state-transition/src/epoch/processRegistryUpdates.ts @@ -1,3 +1,4 @@ +import {ForkSeq} from "@lodestar/params"; import {computeActivationExitEpoch} from "../util/index.js"; import {initiateValidatorExit} from "../block/index.js"; import {EpochTransitionCache, CachedBeaconStateAllForks} from "../types.js"; @@ -16,7 +17,11 @@ import {EpochTransitionCache, CachedBeaconStateAllForks} from "../types.js"; * - indicesEligibleForActivationQueue: 0 * - indicesToEject: 0 */ -export function processRegistryUpdates(state: CachedBeaconStateAllForks, cache: EpochTransitionCache): void { +export function processRegistryUpdates( + fork: ForkSeq, + state: CachedBeaconStateAllForks, + cache: EpochTransitionCache +): void { const {epochCtx} = state; // Get the validators sub tree once for all the loop @@ -28,7 +33,7 @@ export function processRegistryUpdates(state: CachedBeaconStateAllForks, cache: for (const index of cache.indicesToEject) { // set validator exit epoch and withdrawable epoch // TODO: Figure out a way to quickly set properties on the validators tree - initiateValidatorExit(state, validators.get(index)); + initiateValidatorExit(fork, state, validators.get(index)); } // set new activation eligibilities @@ -37,8 +42,15 @@ export function processRegistryUpdates(state: CachedBeaconStateAllForks, cache: } const finalityEpoch = state.finalizedCheckpoint.epoch; - // dequeue validators for activation up to churn limit - for (const index of cache.indicesEligibleForActivation.slice(0, epochCtx.activationChurnLimit)) { + let indicesEligibleForActivation; + if (fork < ForkSeq.electra) { + // dequeue validators for activation up to churn limit + indicesEligibleForActivation = cache.indicesEligibleForActivation.slice(0, epochCtx.activationChurnLimit); + } else { + // no churn limit post-electra + indicesEligibleForActivation = cache.indicesEligibleForActivation; + } + for (const index of indicesEligibleForActivation) { const validator = validators.get(index); // placement in queue is finalized if (validator.activationEligibilityEpoch > finalityEpoch) { diff --git a/packages/state-transition/src/signatureSets/consolidation.ts b/packages/state-transition/src/signatureSets/consolidation.ts new file mode 100644 index 000000000000..0aef47d417a5 --- /dev/null +++ b/packages/state-transition/src/signatureSets/consolidation.ts @@ -0,0 +1,51 @@ +import {DOMAIN_CONSOLIDATION, ForkName} from "@lodestar/params"; +import {electra, ssz} from "@lodestar/types"; + +import { + computeSigningRoot, + createAggregateSignatureSetFromComponents, + ISignatureSet, + verifySignatureSet, +} from "../util/index.js"; +import {CachedBeaconStateElectra} from "../types.js"; + +export function verifyConsolidationSignature( + state: CachedBeaconStateElectra, + signedConsolidation: electra.SignedConsolidation +): boolean { + return verifySignatureSet(getConsolidationSignatureSet(state, signedConsolidation)); +} + +/** + * Extract signatures to allow validating all block signatures at once + */ +export function getConsolidationSignatureSet( + state: CachedBeaconStateElectra, + signedConsolidation: electra.SignedConsolidation +): ISignatureSet { + const {config} = state; + const {index2pubkey} = state.epochCtx; // TODO Electra: Use 6110 pubkey cache + const {sourceIndex, targetIndex} = signedConsolidation.message; + const sourcePubkey = index2pubkey[sourceIndex]; + const targetPubkey = index2pubkey[targetIndex]; + + // signatureFork for signing domain is fixed + const signatureFork = ForkName.phase0; + const domain = config.getDomainAtFork(signatureFork, DOMAIN_CONSOLIDATION); + const signingRoot = computeSigningRoot(ssz.electra.Consolidation, signedConsolidation.message, domain); + + return createAggregateSignatureSetFromComponents( + [sourcePubkey, targetPubkey], + signingRoot, + signedConsolidation.signature + ); +} + +export function getConsolidationSignatureSets( + state: CachedBeaconStateElectra, + signedBlock: electra.SignedBeaconBlock +): ISignatureSet[] { + return signedBlock.message.body.consolidations.map((consolidation) => + getConsolidationSignatureSet(state, consolidation) + ); +} diff --git a/packages/state-transition/src/signatureSets/index.ts b/packages/state-transition/src/signatureSets/index.ts index 05e4ad4b197a..a6bdc7f478bd 100644 --- a/packages/state-transition/src/signatureSets/index.ts +++ b/packages/state-transition/src/signatureSets/index.ts @@ -1,7 +1,7 @@ import {ForkSeq} from "@lodestar/params"; -import {allForks, altair, capella} from "@lodestar/types"; +import {allForks, altair, capella, electra} from "@lodestar/types"; import {ISignatureSet} from "../util/index.js"; -import {CachedBeaconStateAllForks, CachedBeaconStateAltair} from "../types.js"; +import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStateElectra} from "../types.js"; import {getSyncCommitteeSignatureSet} from "../block/processSyncCommittee.js"; import {getProposerSlashingsSignatureSets} from "./proposerSlashings.js"; import {getAttesterSlashingsSignatureSets} from "./attesterSlashings.js"; @@ -10,6 +10,7 @@ import {getBlockProposerSignatureSet} from "./proposer.js"; import {getRandaoRevealSignatureSet} from "./randao.js"; import {getVoluntaryExitsSignatureSets} from "./voluntaryExits.js"; import {getBlsToExecutionChangeSignatureSets} from "./blsToExecutionChange.js"; +import {getConsolidationSignatureSets} from "./consolidation.js"; export * from "./attesterSlashings.js"; export * from "./indexedAttestation.js"; @@ -18,6 +19,7 @@ export * from "./proposerSlashings.js"; export * from "./randao.js"; export * from "./voluntaryExits.js"; export * from "./blsToExecutionChange.js"; +export * from "./consolidation.js"; /** * Includes all signatures on the block (except the deposit signatures) for verification. @@ -69,5 +71,15 @@ export function getBlockSignatureSets( } } + if (fork >= ForkSeq.electra) { + const consolidationSignatureSets = getConsolidationSignatureSets( + state as CachedBeaconStateElectra, + signedBlock as electra.SignedBeaconBlock + ); + if (consolidationSignatureSets.length > 0) { + signatureSets.push(...consolidationSignatureSets); + } + } + return signatureSets; } diff --git a/packages/state-transition/src/slot/upgradeStateToElectra.ts b/packages/state-transition/src/slot/upgradeStateToElectra.ts index f41c37af94aa..760595c3a86b 100644 --- a/packages/state-transition/src/slot/upgradeStateToElectra.ts +++ b/packages/state-transition/src/slot/upgradeStateToElectra.ts @@ -1,7 +1,12 @@ import {ssz} from "@lodestar/types"; -import {UNSET_DEPOSIT_RECEIPTS_START_INDEX} from "@lodestar/params"; +import {FAR_FUTURE_EPOCH, UNSET_DEPOSIT_RECEIPTS_START_INDEX} from "@lodestar/params"; import {CachedBeaconStateDeneb} from "../types.js"; import {CachedBeaconStateElectra, getCachedBeaconState} from "../cache/stateCache.js"; +import { + hasCompoundingWithdrawalCredential, + queueEntireBalanceAndResetValidator, + queueExcessActiveBalance, +} from "../util/electra.js"; /** * Upgrade a state from Capella to Deneb. @@ -24,6 +29,23 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache // default value of depositReceiptsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX stateElectra.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + const validatorsArr = stateElectra.validators.getAllReadonly(); + + for (let i = 0; i < validatorsArr.length; i++) { + const validator = validatorsArr[i]; + + // [EIP-7251]: add validators that are not yet active to pending balance deposits + if (validator.activationEligibilityEpoch === FAR_FUTURE_EPOCH) { + queueEntireBalanceAndResetValidator(stateElectra, i); + } + + // [EIP-7251]: Ensure early adopters of compounding credentials go through the activation churn + const withdrawalCredential = validator.withdrawalCredentials; + if (hasCompoundingWithdrawalCredential(withdrawalCredential)) { + queueExcessActiveBalance(stateElectra, i); + } + } + // Commit new added fields ViewDU to the root node stateElectra.commit(); // Clear cache to ensure the cache of deneb fields is not used by new ELECTRA fields diff --git a/packages/state-transition/src/util/electra.ts b/packages/state-transition/src/util/electra.ts new file mode 100644 index 000000000000..00c5bb183eb4 --- /dev/null +++ b/packages/state-transition/src/util/electra.ts @@ -0,0 +1,103 @@ +import { + COMPOUNDING_WITHDRAWAL_PREFIX, + FAR_FUTURE_EPOCH, + ForkSeq, + MAX_EFFECTIVE_BALANCE, + MIN_ACTIVATION_BALANCE, +} from "@lodestar/params"; +import {ValidatorIndex, phase0, ssz} from "@lodestar/types"; +import {CachedBeaconStateElectra} from "../types.js"; +import {getValidatorMaxEffectiveBalance} from "./validator.js"; +import {hasEth1WithdrawalCredential} from "./capella.js"; + +type ValidatorInfo = Pick; + +export function hasCompoundingWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean { + return withdrawalCredentials[0] === COMPOUNDING_WITHDRAWAL_PREFIX; +} + +export function hasExecutionWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean { + return ( + hasCompoundingWithdrawalCredential(withdrawalCredentials) || hasEth1WithdrawalCredential(withdrawalCredentials) + ); +} + +export function isFullyWithdrawableValidator( + fork: ForkSeq, + validatorCredential: ValidatorInfo, + balance: number, + epoch: number +): boolean { + const {withdrawableEpoch, withdrawalCredentials: withdrawalCredential} = validatorCredential; + + if (fork < ForkSeq.capella) { + throw new Error(`isFullyWithdrawableValidator not supported at forkSeq=${fork} < ForkSeq.capella`); + } + const hasWithdrawableCredentials = + fork >= ForkSeq.electra + ? hasExecutionWithdrawalCredential(withdrawalCredential) + : hasEth1WithdrawalCredential(withdrawalCredential); + + return hasWithdrawableCredentials && withdrawableEpoch <= epoch && balance > 0; +} + +export function isPartiallyWithdrawableValidator( + fork: ForkSeq, + validatorCredential: ValidatorInfo, + balance: number +): boolean { + const {effectiveBalance, withdrawalCredentials: withdrawalCredential} = validatorCredential; + + if (fork < ForkSeq.capella) { + throw new Error(`isPartiallyWithdrawableValidator not supported at forkSeq=${fork} < ForkSeq.capella`); + } + const hasWithdrawableCredentials = + fork >= ForkSeq.electra + ? hasExecutionWithdrawalCredential(withdrawalCredential) + : hasEth1WithdrawalCredential(withdrawalCredential); + + const validatorMaxEffectiveBalance = + fork >= ForkSeq.electra ? getValidatorMaxEffectiveBalance(withdrawalCredential) : MAX_EFFECTIVE_BALANCE; + const hasMaxEffectiveBalance = effectiveBalance === validatorMaxEffectiveBalance; + const hasExcessBalance = balance > validatorMaxEffectiveBalance; + + return hasWithdrawableCredentials && hasMaxEffectiveBalance && hasExcessBalance; +} + +export function switchToCompoundingValidator(state: CachedBeaconStateElectra, index: ValidatorIndex): void { + const validator = state.validators.get(index); + + if (hasEth1WithdrawalCredential(validator.withdrawalCredentials)) { + validator.withdrawalCredentials[0] = COMPOUNDING_WITHDRAWAL_PREFIX; + queueExcessActiveBalance(state, index); + } +} + +export function queueExcessActiveBalance(state: CachedBeaconStateElectra, index: ValidatorIndex): void { + const balance = state.balances.get(index); + if (balance > MIN_ACTIVATION_BALANCE) { + const excessBalance = balance - MIN_ACTIVATION_BALANCE; + state.balances.set(index, MIN_ACTIVATION_BALANCE); + + const pendingBalanceDeposit = ssz.electra.PendingBalanceDeposit.toViewDU({ + index, + amount: BigInt(excessBalance), + }); + state.pendingBalanceDeposits.push(pendingBalanceDeposit); + } +} + +export function queueEntireBalanceAndResetValidator(state: CachedBeaconStateElectra, index: ValidatorIndex): void { + const balance = state.balances.get(index); + state.balances.set(index, 0); + + const validator = state.validators.get(index); + validator.effectiveBalance = 0; + validator.activationEligibilityEpoch = FAR_FUTURE_EPOCH; + + const pendingBalanceDeposit = ssz.electra.PendingBalanceDeposit.toViewDU({ + index, + amount: BigInt(balance), + }); + state.pendingBalanceDeposits.push(pendingBalanceDeposit); +} diff --git a/packages/state-transition/src/util/epoch.ts b/packages/state-transition/src/util/epoch.ts index b7febf45c110..e399d3276e3e 100644 --- a/packages/state-transition/src/util/epoch.ts +++ b/packages/state-transition/src/util/epoch.ts @@ -1,5 +1,7 @@ import {EPOCHS_PER_SYNC_COMMITTEE_PERIOD, GENESIS_EPOCH, MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params"; -import {allForks, Epoch, Slot, SyncPeriod} from "@lodestar/types"; +import {allForks, Epoch, Gwei, Slot, SyncPeriod} from "@lodestar/types"; +import {CachedBeaconStateElectra} from "../types.js"; +import {getActivationExitChurnLimit, getConsolidationChurnLimit} from "./validator.js"; /** * Return the epoch number at the given slot. @@ -39,6 +41,60 @@ export function computeActivationExitEpoch(epoch: Epoch): Epoch { return epoch + 1 + MAX_SEED_LOOKAHEAD; } +export function computeExitEpochAndUpdateChurn(state: CachedBeaconStateElectra, exitBalance: Gwei): number { + let earliestExitEpoch = Math.max(state.earliestExitEpoch, computeActivationExitEpoch(state.epochCtx.epoch)); + const perEpochChurn = getActivationExitChurnLimit(state); + + // New epoch for exits. + let exitBalanceToConsume = + state.earliestExitEpoch < earliestExitEpoch ? perEpochChurn : Number(state.exitBalanceToConsume); + + // Exit doesn't fit in the current earliest epoch. + if (exitBalance > exitBalanceToConsume) { + const balanceToProcess = Number(exitBalance) - exitBalanceToConsume; + const additionalEpochs = Math.floor((balanceToProcess - 1) / (perEpochChurn + 1)); + earliestExitEpoch += additionalEpochs; + exitBalanceToConsume += additionalEpochs * perEpochChurn; + } + + // Consume the balance and update state variables. + state.exitBalanceToConsume = BigInt(exitBalanceToConsume) - exitBalance; + state.earliestExitEpoch = earliestExitEpoch; + + return state.earliestExitEpoch; +} + +export function computeConsolidationEpochAndUpdateChurn( + state: CachedBeaconStateElectra, + consolidationBalance: Gwei +): number { + let earliestConsolidationEpoch = Math.max( + state.earliestConsolidationEpoch, + computeActivationExitEpoch(state.epochCtx.epoch) + ); + const perEpochConsolidationChurn = getConsolidationChurnLimit(state); + + // New epoch for consolidations + let consolidationBalanceToConsume = + state.earliestConsolidationEpoch < earliestConsolidationEpoch + ? perEpochConsolidationChurn + : Number(state.consolidationBalanceToConsume); + + // Consolidation doesn't fit in the current earliest epoch. + if (consolidationBalance > consolidationBalanceToConsume) { + const balanceToProcess = Number(consolidationBalance) - consolidationBalanceToConsume; + const additionalEpochs = Math.floor((balanceToProcess - 1) / (perEpochConsolidationChurn + 1)); + earliestConsolidationEpoch += additionalEpochs; + consolidationBalanceToConsume += additionalEpochs * perEpochConsolidationChurn; + } + + // Consume the balance and update state variables. + state.consolidationBalanceToConsume = BigInt(consolidationBalanceToConsume) - consolidationBalance; + state.earliestConsolidationEpoch = earliestConsolidationEpoch; + + return state.earliestConsolidationEpoch; +} + /** * Return the current epoch of the given state. */ diff --git a/packages/state-transition/src/util/genesis.ts b/packages/state-transition/src/util/genesis.ts index 6c700436d7f6..537edc7f976f 100644 --- a/packages/state-transition/src/util/genesis.ts +++ b/packages/state-transition/src/util/genesis.ts @@ -185,7 +185,7 @@ export function applyDeposits( validator.effectiveBalance = effectiveBalance; epochCtx.effectiveBalanceIncrementsSet(i, effectiveBalance); - if (validator.effectiveBalance === MAX_EFFECTIVE_BALANCE) { + if (validator.effectiveBalance >= MAX_EFFECTIVE_BALANCE) { validator.activationEligibilityEpoch = GENESIS_EPOCH; validator.activationEpoch = GENESIS_EPOCH; activatedValidatorCount++; diff --git a/packages/state-transition/src/util/index.ts b/packages/state-transition/src/util/index.ts index ba998f65b254..6a839fbe103d 100644 --- a/packages/state-transition/src/util/index.ts +++ b/packages/state-transition/src/util/index.ts @@ -24,3 +24,4 @@ export * from "./syncCommittee.js"; export * from "./validator.js"; export * from "./weakSubjectivity.js"; export * from "./deposit.js"; +export * from "./electra.js"; diff --git a/packages/state-transition/src/util/validator.ts b/packages/state-transition/src/util/validator.ts index 99f1e6fa0b19..0c8c74b2cb03 100644 --- a/packages/state-transition/src/util/validator.ts +++ b/packages/state-transition/src/util/validator.ts @@ -1,8 +1,14 @@ import {Epoch, phase0, ValidatorIndex} from "@lodestar/types"; import {intDiv} from "@lodestar/utils"; import {ChainForkConfig} from "@lodestar/config"; -import {ForkSeq} from "@lodestar/params"; -import {BeaconStateAllForks} from "../types.js"; +import { + EFFECTIVE_BALANCE_INCREMENT, + ForkSeq, + MAX_EFFECTIVE_BALANCE_ELECTRA, + MIN_ACTIVATION_BALANCE, +} from "@lodestar/params"; +import {BeaconStateAllForks, CachedBeaconStateElectra} from "../types.js"; +import {hasCompoundingWithdrawalCredential} from "./electra.js"; /** * Check if [[validator]] is active @@ -47,3 +53,48 @@ export function getActivationChurnLimit(config: ChainForkConfig, fork: ForkSeq, export function getChurnLimit(config: ChainForkConfig, activeValidatorCount: number): number { return Math.max(config.MIN_PER_EPOCH_CHURN_LIMIT, intDiv(activeValidatorCount, config.CHURN_LIMIT_QUOTIENT)); } + +/** + * Get combined churn limit of activation-exit and consolidation + */ +export function getBalanceChurnLimit(state: CachedBeaconStateElectra): number { + const churnLimitByTotalActiveBalance = Math.floor( + (state.epochCtx.totalActiveBalanceIncrements / state.config.CHURN_LIMIT_QUOTIENT) * EFFECTIVE_BALANCE_INCREMENT + ); // TODO Electra: verify calculation + + const churn = Math.max(churnLimitByTotalActiveBalance, state.config.MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA); + + return churn - (churn % EFFECTIVE_BALANCE_INCREMENT); +} + +export function getActivationExitChurnLimit(state: CachedBeaconStateElectra): number { + return Math.min(state.config.MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT, getBalanceChurnLimit(state)); +} + +export function getConsolidationChurnLimit(state: CachedBeaconStateElectra): number { + return getBalanceChurnLimit(state) - getActivationExitChurnLimit(state); +} + +export function getValidatorMaxEffectiveBalance(withdrawalCredentials: Uint8Array): number { + // Compounding withdrawal credential only available since Electra + if (hasCompoundingWithdrawalCredential(withdrawalCredentials)) { + return MAX_EFFECTIVE_BALANCE_ELECTRA; + } else { + return MIN_ACTIVATION_BALANCE; + } +} + +export function getActiveBalance(state: CachedBeaconStateElectra, validatorIndex: ValidatorIndex): number { + const validatorMaxEffectiveBalance = getValidatorMaxEffectiveBalance( + state.validators.getReadonly(validatorIndex).withdrawalCredentials + ); + + return Math.min(state.balances.get(validatorIndex), validatorMaxEffectiveBalance); +} + +export function getPendingBalanceToWithdraw(state: CachedBeaconStateElectra, validatorIndex: ValidatorIndex): number { + return state.pendingPartialWithdrawals + .getAllReadonly() + .filter((item) => item.index === validatorIndex) + .reduce((total, item) => total + Number(item.amount), 0); +} diff --git a/packages/state-transition/test/perf/analyzeEpochs.ts b/packages/state-transition/test/perf/analyzeEpochs.ts index 4b793fe95e6e..c7b88653d00c 100644 --- a/packages/state-transition/test/perf/analyzeEpochs.ts +++ b/packages/state-transition/test/perf/analyzeEpochs.ts @@ -152,6 +152,9 @@ async function analyzeEpochs(network: NetworkName, fromEpoch?: number): Promise< // processRegistryUpdates: function of registry updates // processSlashingsAllForks: function of process.indicesToSlash // processSlashingsReset: free + // -- electra + // processPendingBalanceDeposits: - + // processPendingConsolidations: - // -- altair // processInactivityUpdates: - // processParticipationFlagUpdates: - diff --git a/packages/state-transition/test/perf/block/processWithdrawals.test.ts b/packages/state-transition/test/perf/block/processWithdrawals.test.ts index 997f401d32ce..66d624b39bfd 100644 --- a/packages/state-transition/test/perf/block/processWithdrawals.test.ts +++ b/packages/state-transition/test/perf/block/processWithdrawals.test.ts @@ -1,4 +1,5 @@ import {itBench} from "@dapplion/benchmark"; +import {ForkSeq} from "@lodestar/params"; import {CachedBeaconStateCapella} from "../../../src/index.js"; import {getExpectedWithdrawals} from "../../../src/block/processWithdrawals.js"; import {numValidators} from "../util.js"; @@ -9,7 +10,7 @@ import {getExpectedWithdrawalsTestData, WithdrawalOpts} from "../../utils/capell // having BLS withdrawal credential prefix as that validator probe is wasted. // // Best case: -// All Validator have balances > MAX_EFFECTIVE_BALANCE and ETH1 withdrawal credential prefix set +// All Validator have balances > MAX_EFFECTIVE_BALANCE and ETH1 withdrawal credential prefix set // TODO Electra: Not true anymore // // Worst case: // All balances are low enough or withdrawal credential not set @@ -69,7 +70,7 @@ describe("getExpectedWithdrawals", () => { return opts.cache ? state : state.clone(true); }, fn: (state) => { - const {sampledValidators} = getExpectedWithdrawals(state); + const {sampledValidators} = getExpectedWithdrawals(ForkSeq.capella, state); // TODO Electra: Do test for electra if (sampledValidators !== opts.sampled) { throw Error(`Wrong sampledValidators ${sampledValidators} != ${opts.sampled}`); } diff --git a/packages/state-transition/test/unit/block/processWithdrawals.test.ts b/packages/state-transition/test/unit/block/processWithdrawals.test.ts index 2841da635472..7b708d108a7b 100644 --- a/packages/state-transition/test/unit/block/processWithdrawals.test.ts +++ b/packages/state-transition/test/unit/block/processWithdrawals.test.ts @@ -1,4 +1,5 @@ import {describe, it, expect} from "vitest"; +import {ForkSeq} from "@lodestar/params"; import {getExpectedWithdrawals} from "../../../src/block/processWithdrawals.js"; import {numValidators} from "../../perf/util.js"; import {getExpectedWithdrawalsTestData, WithdrawalOpts} from "../../utils/capella.js"; @@ -36,8 +37,9 @@ describe("getExpectedWithdrawals", () => { // Clone true to drop cache const state = beforeValue(() => getExpectedWithdrawalsTestData(vc, opts).clone(true)); + // TODO Electra: Add test for electra it(`getExpectedWithdrawals ${vc} ${caseID}`, () => { - const {sampledValidators, withdrawals} = getExpectedWithdrawals(state.value); + const {sampledValidators, withdrawals} = getExpectedWithdrawals(ForkSeq.capella, state.value); expect(sampledValidators).toBe(opts.sampled); expect(withdrawals.length).toBe(opts.withdrawals); }); diff --git a/packages/types/package.json b/packages/types/package.json index 920a490e8205..56546d686315 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -76,7 +76,7 @@ }, "types": "lib/index.d.ts", "dependencies": { - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "^0.16.0", "@lodestar/params": "^1.19.0", "ethereum-cryptography": "^2.0.0" }, diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index 53195f30de11..26539dd61365 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -17,6 +17,10 @@ import { MAX_ATTESTATIONS_ELECTRA, MAX_ATTESTER_SLASHINGS_ELECTRA, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, + MAX_CONSOLIDATIONS, + PENDING_BALANCE_DEPOSITS_LIMIT, + PENDING_CONSOLIDATIONS_LIMIT, + PENDING_PARTIAL_WITHDRAWALS_LIMIT, } from "@lodestar/params"; import {ssz as primitiveSsz} from "../primitive/index.js"; import {ssz as phase0Ssz} from "../phase0/index.js"; @@ -26,6 +30,8 @@ import {ssz as capellaSsz} from "../capella/index.js"; import {ssz as denebSsz} from "../deneb/index.js"; const { + Epoch, + Gwei, UintNum64, Slot, Root, @@ -148,6 +154,23 @@ export const ExecutionPayloadHeader = new ContainerType( {typeName: "ExecutionPayloadHeader", jsonCase: "eth2"} ); +export const Consolidation = new ContainerType( + { + sourceIndex: ValidatorIndex, + targetIndex: ValidatorIndex, + epoch: Epoch, + }, + {typeName: "Consolidation", jsonCase: "eth2"} +); + +export const SignedConsolidation = new ContainerType( + { + message: Consolidation, + signature: BLSSignature, + }, + {typeName: "SignedConsolidation", jsonCase: "eth2"} +); + // We have to preserve Fields ordering while changing the type of ExecutionPayload export const BeaconBlockBody = new ContainerType( { @@ -163,6 +186,7 @@ export const BeaconBlockBody = new ContainerType( executionPayload: ExecutionPayload, // Modified in ELECTRA blsToExecutionChanges: capellaSsz.BeaconBlockBody.fields.blsToExecutionChanges, blobKzgCommitments: denebSsz.BeaconBlockBody.fields.blobKzgCommitments, + consolidations: new ListCompositeType(SignedConsolidation, MAX_CONSOLIDATIONS), // [New in Electra] }, {typeName: "BeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} ); @@ -243,8 +267,32 @@ export const ExecutionPayloadAndBlobsBundle = new ContainerType( {typeName: "ExecutionPayloadAndBlobsBundle", jsonCase: "eth2"} ); -// We don't spread deneb.BeaconState fields since we need to replace -// latestExecutionPayloadHeader and we cannot keep order doing that +export const PendingBalanceDeposit = new ContainerType( + { + index: ValidatorIndex, + amount: Gwei, + }, + {typeName: "PendingBalanceDeposit", jsonCase: "eth2"} +); + +export const PartialWithdrawal = new ContainerType( + { + index: ValidatorIndex, + amount: Gwei, + withdrawableEpoch: Epoch, + }, + {typeName: "PartialWithdrawal", jsonCase: "eth2"} +); + +export const PendingConsolidation = new ContainerType( + { + sourceIndex: ValidatorIndex, + targetIndex: ValidatorIndex, + }, + {typeName: "PendingConsolidation", jsonCase: "eth2"} +); + +// In EIP-7251, we spread deneb fields as new fields are appended at the end export const BeaconState = new ContainerType( { genesisTime: UintNum64, @@ -288,6 +336,14 @@ export const BeaconState = new ContainerType( // Deep history valid from Capella onwards historicalSummaries: capellaSsz.BeaconState.fields.historicalSummaries, depositReceiptsStartIndex: UintBn64, // New in ELECTRA + depositBalanceToConsume: Gwei, // [New in Electra] + exitBalanceToConsume: Gwei, // [New in Electra] + earliestExitEpoch: Epoch, // [New in Electra] + consolidationBalanceToConsume: Gwei, // [New in Electra] + earliestConsolidationEpoch: Epoch, // [New in Electra] + pendingBalanceDeposits: new ListCompositeType(PendingBalanceDeposit, PENDING_BALANCE_DEPOSITS_LIMIT), // [New in Electra] + pendingPartialWithdrawals: new ListCompositeType(PartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT), // [New in Electra] + pendingConsolidations: new ListCompositeType(PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT), // [New in Electra] }, {typeName: "BeaconState", jsonCase: "eth2"} ); diff --git a/packages/types/src/electra/types.ts b/packages/types/src/electra/types.ts index d1d0109e6ae6..4de209b48960 100644 --- a/packages/types/src/electra/types.ts +++ b/packages/types/src/electra/types.ts @@ -42,3 +42,10 @@ export type LightClientUpdate = ValueOf; export type LightClientFinalityUpdate = ValueOf; export type LightClientOptimisticUpdate = ValueOf; export type LightClientStore = ValueOf; + +export type Consolidation = ValueOf; +export type SignedConsolidation = ValueOf; + +export type PendingBalanceDeposit = ValueOf; +export type PartialWithdrawal = ValueOf; +export type PendingConsolidation = ValueOf; diff --git a/packages/types/src/phase0/sszTypes.ts b/packages/types/src/phase0/sszTypes.ts index 9eb2a13e5fae..4a04701b789d 100644 --- a/packages/types/src/phase0/sszTypes.ts +++ b/packages/types/src/phase0/sszTypes.ts @@ -236,7 +236,7 @@ export const RandaoMixes = new VectorCompositeType(Bytes32, EPOCHS_PER_HISTORICA * This is initially a Gwei (BigInt) vector, however since Nov 2023 it's converted to UintNum64 (number) vector in the state transition because: * - state.slashings[nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR] is reset per epoch in processSlashingsReset() * - max slashed validators per epoch is SLOTS_PER_EPOCH * MAX_ATTESTER_SLASHINGS * MAX_VALIDATORS_PER_COMMITTEE which is 32 * 2 * 2048 = 131072 on mainnet - * - with that and 32_000_000_000 MAX_EFFECTIVE_BALANCE, it still fits in a number given that Math.floor(Number.MAX_SAFE_INTEGER / 32_000_000_000) = 281474 + * - with that and 32_000_000_000 MAX_EFFECTIVE_BALANCE or 2048_000_000_000 MAX_EFFECTIVE_BALANCE_ELECTRA, it still fits in a number given that Math.floor(Number.MAX_SAFE_INTEGER / 32_000_000_000) = 281474 * - we don't need to compute the total slashings from state.slashings, it's handled by totalSlashingsByIncrement in EpochCache */ export const Slashings = new VectorBasicType(UintNum64, EPOCHS_PER_SLASHINGS_VECTOR); diff --git a/packages/validator/package.json b/packages/validator/package.json index 7bd1cdae0ac7..42df23ae5d9d 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -45,8 +45,8 @@ "blockchain" ], "dependencies": { - "@chainsafe/bls": "7.1.3", - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/bls": "^8.1.0", + "@chainsafe/ssz": "^0.16.0", "@lodestar/api": "^1.19.0", "@lodestar/config": "^1.19.0", "@lodestar/db": "^1.19.0", diff --git a/packages/validator/src/util/params.ts b/packages/validator/src/util/params.ts index ff1c8c0fdc25..3a497555361a 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -227,5 +227,16 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record Date: Tue, 7 May 2024 22:11:51 +0300 Subject: [PATCH 10/51] feat: beacon node process electra attestations EIP-7549 (#6738) * Process attestations in block * Fix check-types * Address comments --- .../src/chain/blocks/importBlock.ts | 4 +- .../test/spec/presets/fork_choice.test.ts | 5 +- .../src/block/processAttestationPhase0.ts | 57 +++++++++--- .../src/block/processAttestations.ts | 4 +- .../src/block/processAttestationsAltair.ts | 7 +- .../state-transition/src/cache/epochCache.ts | 90 ++++++++++++++++--- .../src/signatureSets/indexedAttestation.ts | 6 +- 7 files changed, 138 insertions(+), 35 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 9c467c26ca50..71645ae6b4f8 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -69,6 +69,7 @@ export async function importBlock( const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; const blockDelaySec = (fullyVerifiedBlock.seenTimestampSec - postState.genesisTime) % this.config.SECONDS_PER_SLOT; const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000); + const fork = this.config.getForkSeq(blockSlot); // this is just a type assertion since blockinput with dataPromise type will not end up here if (blockInput.type === BlockInputType.dataPromise) { @@ -148,7 +149,8 @@ export async function importBlock( for (const attestation of attestations) { try { - const indexedAttestation = postState.epochCtx.getIndexedAttestation(attestation); + // TODO Electra: figure out how to reuse the attesting indices computed from state transition + const indexedAttestation = postState.epochCtx.getIndexedAttestation(fork, attestation); const {target, beaconBlockRoot} = attestation.data; const attDataRoot = toHexString(ssz.phase0.AttestationData.hashTreeRoot(indexedAttestation.data)); diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index 67cb35972d77..98471dffba39 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -137,7 +137,10 @@ const forkChoiceTest = if (!attestation) throw Error(`No attestation ${step.attestation}`); const headState = chain.getHeadState(); const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(attestation.data)); - chain.forkChoice.onAttestation(headState.epochCtx.getIndexedAttestation(attestation), attDataRootHex); + chain.forkChoice.onAttestation( + headState.epochCtx.getIndexedAttestation(ForkSeq[fork], attestation), + attDataRootHex + ); } // attester slashing step diff --git a/packages/state-transition/src/block/processAttestationPhase0.ts b/packages/state-transition/src/block/processAttestationPhase0.ts index 248ba83b4ed2..9cd1823e4dd4 100644 --- a/packages/state-transition/src/block/processAttestationPhase0.ts +++ b/packages/state-transition/src/block/processAttestationPhase0.ts @@ -1,7 +1,8 @@ import {toHexString} from "@chainsafe/ssz"; -import {Slot, phase0, ssz} from "@lodestar/types"; +import {Slot, allForks, electra, phase0, ssz} from "@lodestar/types"; import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH, ForkSeq} from "@lodestar/params"; +import {assert} from "@lodestar/utils"; import {computeEpochAtSlot} from "../util/index.js"; import {CachedBeaconStatePhase0, CachedBeaconStateAllForks} from "../types.js"; import {isValidIndexedAttestation} from "./index.js"; @@ -51,7 +52,7 @@ export function processAttestationPhase0( state.previousEpochAttestations.push(pendingAttestation); } - if (!isValidIndexedAttestation(state, epochCtx.getIndexedAttestation(attestation), verifySignature)) { + if (!isValidIndexedAttestation(state, epochCtx.getIndexedAttestation(ForkSeq.phase0, attestation), verifySignature)) { throw new Error("Attestation is not valid"); } } @@ -59,19 +60,14 @@ export function processAttestationPhase0( export function validateAttestation( fork: ForkSeq, state: CachedBeaconStateAllForks, - attestation: phase0.Attestation + attestation: allForks.Attestation ): void { const {epochCtx} = state; const slot = state.slot; const data = attestation.data; const computedEpoch = computeEpochAtSlot(data.slot); const committeeCount = epochCtx.getCommitteeCountPerSlot(computedEpoch); - if (!(data.index < committeeCount)) { - throw new Error( - "Attestation committee index not within current committee count: " + - `committeeIndex=${data.index} committeeCount=${committeeCount}` - ); - } + if (!(data.target.epoch === epochCtx.previousShuffling.epoch || data.target.epoch === epochCtx.epoch)) { throw new Error( "Attestation target epoch not in previous or current epoch: " + @@ -93,12 +89,45 @@ export function validateAttestation( ); } - const committee = epochCtx.getBeaconCommittee(data.slot, data.index); - if (attestation.aggregationBits.bitLen !== committee.length) { - throw new Error( - "Attestation aggregation bits length does not match committee length: " + - `aggregationBitsLength=${attestation.aggregationBits.bitLen} committeeLength=${committee.length}` + if (fork >= ForkSeq.electra) { + assert.equal(data.index, 0, `AttestationData.index must be zero: index=${data.index}`); + const attestationElectra = attestation as electra.Attestation; + const committeeBitsLength = attestationElectra.committeeBits.bitLen; + + if (committeeBitsLength > committeeCount) { + throw new Error( + `Attestation committee bits length are longer than number of committees: committeeBitsLength=${committeeBitsLength} numCommittees=${committeeCount}` + ); + } + + // TODO Electra: this should be obsolete soon when the spec switches to committeeIndices + const committeeIndices = attestationElectra.committeeBits.getTrueBitIndexes(); + + // Get total number of attestation participant of every committee specified + const participantCount = committeeIndices + .map((committeeIndex) => epochCtx.getBeaconCommittee(data.slot, committeeIndex).length) + .reduce((acc, committeeSize) => acc + committeeSize, 0); + + assert.equal( + attestationElectra.aggregationBits.bitLen, + participantCount, + `Attestation aggregation bits length does not match total number of committee participant aggregationBitsLength=${attestation.aggregationBits.bitLen} participantCount=${participantCount}` ); + } else { + if (!(data.index < committeeCount)) { + throw new Error( + "Attestation committee index not within current committee count: " + + `committeeIndex=${data.index} committeeCount=${committeeCount}` + ); + } + + const committee = epochCtx.getBeaconCommittee(data.slot, data.index); + if (attestation.aggregationBits.bitLen !== committee.length) { + throw new Error( + "Attestation aggregation bits length does not match committee length: " + + `aggregationBitsLength=${attestation.aggregationBits.bitLen} committeeLength=${committee.length}` + ); + } } } diff --git a/packages/state-transition/src/block/processAttestations.ts b/packages/state-transition/src/block/processAttestations.ts index 2b132fa22e0b..991ba5621905 100644 --- a/packages/state-transition/src/block/processAttestations.ts +++ b/packages/state-transition/src/block/processAttestations.ts @@ -1,4 +1,4 @@ -import {phase0} from "@lodestar/types"; +import {allForks} from "@lodestar/types"; import {ForkSeq} from "@lodestar/params"; import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStatePhase0} from "../types.js"; import {processAttestationPhase0} from "./processAttestationPhase0.js"; @@ -10,7 +10,7 @@ import {processAttestationsAltair} from "./processAttestationsAltair.js"; export function processAttestations( fork: ForkSeq, state: CachedBeaconStateAllForks, - attestations: phase0.Attestation[], + attestations: allForks.Attestation[], verifySignatures = true ): void { if (fork === ForkSeq.phase0) { diff --git a/packages/state-transition/src/block/processAttestationsAltair.ts b/packages/state-transition/src/block/processAttestationsAltair.ts index e37629712194..48d304c08133 100644 --- a/packages/state-transition/src/block/processAttestationsAltair.ts +++ b/packages/state-transition/src/block/processAttestationsAltair.ts @@ -1,5 +1,5 @@ import {byteArrayEquals} from "@chainsafe/ssz"; -import {Epoch, phase0} from "@lodestar/types"; +import {Epoch, allForks, phase0} from "@lodestar/types"; import {intSqrt} from "@lodestar/utils"; import { @@ -32,7 +32,7 @@ const SLOTS_PER_EPOCH_SQRT = intSqrt(SLOTS_PER_EPOCH); export function processAttestationsAltair( fork: ForkSeq, state: CachedBeaconStateAltair, - attestations: phase0.Attestation[], + attestations: allForks.Attestation[], verifySignature = true ): void { const {epochCtx} = state; @@ -49,8 +49,7 @@ export function processAttestationsAltair( validateAttestation(fork, state, attestation); // Retrieve the validator indices from the attestation participation bitfield - const committeeIndices = epochCtx.getBeaconCommittee(data.slot, data.index); - const attestingIndices = attestation.aggregationBits.intersectValues(committeeIndices); + const attestingIndices = epochCtx.getAttestingIndices(fork, attestation); // this check is done last because its the most expensive (if signature verification is toggled on) // TODO: Why should we verify an indexed attestation that we just created? If it's just for the signature diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 804f606ab76a..af1362b55a7e 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -2,7 +2,17 @@ import {CoordType, PublicKey} from "@chainsafe/bls/types"; import bls from "@chainsafe/bls"; import * as immutable from "immutable"; import {fromHexString} from "@chainsafe/ssz"; -import {BLSSignature, CommitteeIndex, Epoch, Slot, ValidatorIndex, phase0, SyncPeriod} from "@lodestar/types"; +import { + BLSSignature, + CommitteeIndex, + Epoch, + Slot, + ValidatorIndex, + phase0, + SyncPeriod, + allForks, + electra, +} from "@lodestar/types"; import {createBeaconConfig, BeaconConfig, ChainConfig} from "@lodestar/config"; import { ATTESTATION_SUBNET_COUNT, @@ -645,15 +655,47 @@ export class EpochCache { * Return the beacon committee at slot for index. */ getBeaconCommittee(slot: Slot, index: CommitteeIndex): Uint32Array { + return this.getBeaconCommittees(slot, [index]); + } + + /** + * Return a single Uint32Array representing concatted committees of indices + */ + getBeaconCommittees(slot: Slot, indices: CommitteeIndex[]): Uint32Array { + if (indices.length === 0) { + throw new Error("Attempt to get committees without providing CommitteeIndex"); + } + const slotCommittees = this.getShufflingAtSlot(slot).committees[slot % SLOTS_PER_EPOCH]; - if (index >= slotCommittees.length) { - throw new EpochCacheError({ - code: EpochCacheErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE, - index, - maxIndex: slotCommittees.length, - }); + const committees = []; + + for (const index of indices) { + if (index >= slotCommittees.length) { + throw new EpochCacheError({ + code: EpochCacheErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE, + index, + maxIndex: slotCommittees.length, + }); + } + committees.push(slotCommittees[index]); + } + + // Early return if only one index + if (committees.length === 1) { + return committees[0]; + } + + // Create a new Uint32Array to flatten `committees` + const totalLength = committees.reduce((acc, curr) => acc + curr.length, 0); + const result = new Uint32Array(totalLength); + + let offset = 0; + for (const committee of committees) { + result.set(committee, offset); + offset += committee.length; } - return slotCommittees[index]; + + return result; } getCommitteeCountPerSlot(epoch: Epoch): number { @@ -739,10 +781,9 @@ export class EpochCache { /** * Return the indexed attestation corresponding to ``attestation``. */ - getIndexedAttestation(attestation: phase0.Attestation): phase0.IndexedAttestation { - const {aggregationBits, data} = attestation; - const committeeIndices = this.getBeaconCommittee(data.slot, data.index); - const attestingIndices = aggregationBits.intersectValues(committeeIndices); + getIndexedAttestation(fork: ForkSeq, attestation: allForks.Attestation): allForks.IndexedAttestation { + const {data} = attestation; + const attestingIndices = this.getAttestingIndices(fork, attestation); // sort in-place attestingIndices.sort((a, b) => a - b); @@ -753,6 +794,31 @@ export class EpochCache { }; } + /** + * Return indices of validators who attestested in `attestation` + */ + getAttestingIndices(fork: ForkSeq, attestation: allForks.Attestation): number[] { + if (fork < ForkSeq.electra) { + const {aggregationBits, data} = attestation; + const validatorIndices = this.getBeaconCommittee(data.slot, data.index); + + return aggregationBits.intersectValues(validatorIndices); + } else { + const {aggregationBits, committeeBits, data} = attestation as electra.Attestation; + + // There is a naming conflict on the term `committeeIndices` + // In Lodestar it usually means a list of validator indices of participants in a committee + // In the spec it means a list of committee indices according to committeeBits + // This `committeeIndices` refers to the latter + // TODO Electra: resolve the naming conflicts + const committeeIndices = committeeBits.getTrueBitIndexes(); + + const validatorIndices = this.getBeaconCommittees(data.slot, committeeIndices); + + return aggregationBits.intersectValues(validatorIndices); + } + } + getCommitteeAssignments( epoch: Epoch, requestedValidatorIndices: ValidatorIndex[] diff --git a/packages/state-transition/src/signatureSets/indexedAttestation.ts b/packages/state-transition/src/signatureSets/indexedAttestation.ts index b5c48a20c9d4..cb4706c645fe 100644 --- a/packages/state-transition/src/signatureSets/indexedAttestation.ts +++ b/packages/state-transition/src/signatureSets/indexedAttestation.ts @@ -41,7 +41,11 @@ export function getAttestationsSignatureSets( state: CachedBeaconStateAllForks, signedBlock: allForks.SignedBeaconBlock ): ISignatureSet[] { + // TODO: figure how to get attesting indices of an attestation once per block processing return signedBlock.message.body.attestations.map((attestation) => - getIndexedAttestationSignatureSet(state, state.epochCtx.getIndexedAttestation(attestation)) + getIndexedAttestationSignatureSet( + state, + state.epochCtx.getIndexedAttestation(state.epochCtx.config.getForkSeq(signedBlock.message.slot), attestation) + ) ); } From 7050f8ec1f1e8f4cef862366d849fb41fdeec2a7 Mon Sep 17 00:00:00 2001 From: g11tech Date: Wed, 8 May 2024 16:01:26 +0530 Subject: [PATCH 11/51] feat: handle the EL payload sending data in deposit requests instead of deposit receipts (#6746) --- .../beacon-node/src/execution/engine/types.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index b72b00bc2c4d..c5d798c9bf5e 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -118,7 +118,9 @@ type ExecutionPayloadResponse = ExecutionPayloadRpc | ExecutionPayloadRpcWithVal export type ExecutionPayloadBodyRpc = { transactions: DATA[]; withdrawals: WithdrawalV1[] | null | undefined; - depositReceipts: DepositReceiptV1[] | null | undefined; + // currently there is a discepancy between EL and CL field name references for deposit requests + // its likely CL receipt will be renamed to requests + depositRequests: DepositReceiptV1[] | null | undefined; withdrawalRequests: ExecutionLayerWithdrawalRequestV1[] | null | undefined; }; @@ -148,7 +150,7 @@ export type ExecutionPayloadRpc = { blobGasUsed?: QUANTITY; // DENEB excessBlobGas?: QUANTITY; // DENEB parentBeaconBlockRoot?: QUANTITY; // DENEB - depositReceipts?: DepositReceiptRpc[]; // ELECTRA + depositRequests?: DepositReceiptRpc[]; // ELECTRA withdrawalRequests?: ExecutionLayerWithdrawalRequestRpc[]; // ELECTRA }; @@ -213,10 +215,10 @@ export function serializeExecutionPayload(fork: ForkName, data: allForks.Executi payload.excessBlobGas = numToQuantity(excessBlobGas); } - // ELECTRA adds depositReceipts to the ExecutionPayload + // ELECTRA adds depositReceipts/depositRequests to the ExecutionPayload if (ForkSeq[fork] >= ForkSeq.electra) { const {depositReceipts, withdrawalRequests} = data as electra.ExecutionPayload; - payload.depositReceipts = depositReceipts.map(serializeDepositReceipt); + payload.depositRequests = depositReceipts.map(serializeDepositReceipt); payload.withdrawalRequests = withdrawalRequests.map(serializeExecutionLayerWithdrawalRequest); } @@ -306,14 +308,15 @@ export function parseExecutionPayload( } if (ForkSeq[fork] >= ForkSeq.electra) { - const {depositReceipts, withdrawalRequests} = data; + // electra adds depositRequests/depositReceipts + const {depositRequests, withdrawalRequests} = data; // Geth can also reply with null - if (depositReceipts == null) { + if (depositRequests == null) { throw Error( - `depositReceipts missing for ${fork} >= electra executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}` + `depositRequests missing for ${fork} >= electra executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}` ); } - (executionPayload as electra.ExecutionPayload).depositReceipts = depositReceipts.map(deserializeDepositReceipt); + (executionPayload as electra.ExecutionPayload).depositReceipts = depositRequests.map(deserializeDepositReceipt); if (withdrawalRequests == null) { throw Error( @@ -436,7 +439,7 @@ export function deserializeExecutionPayloadBody(data: ExecutionPayloadBodyRpc | ? { transactions: data.transactions.map((tran) => dataToBytes(tran, null)), withdrawals: data.withdrawals ? data.withdrawals.map(deserializeWithdrawal) : null, - depositReceipts: data.depositReceipts ? data.depositReceipts.map(deserializeDepositReceipt) : null, + depositReceipts: data.depositRequests ? data.depositRequests.map(deserializeDepositReceipt) : null, withdrawalRequests: data.withdrawalRequests ? data.withdrawalRequests.map(deserializeExecutionLayerWithdrawalRequest) : null, @@ -449,7 +452,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, + depositRequests: data.depositReceipts ? data.depositReceipts.map(serializeDepositReceipt) : null, withdrawalRequests: data.withdrawalRequests ? data.withdrawalRequests.map(serializeExecutionLayerWithdrawalRequest) : null, From 825f488fa2e040674b920a0853c338ebe7385da2 Mon Sep 17 00:00:00 2001 From: NC Date: Wed, 8 May 2024 14:00:11 +0300 Subject: [PATCH 12/51] feat: implement EIP-7549 (#6689) * initial commit * lint * Add getAttestingIndices and update getIndexedAttestation * Update gossip validation * Update attestation gossip validation * aggregateAndProof validation * clean up * Validator * Misc * Fix the build erros * feat: get attestations for electra block (#6732) * feat: getAttestationsForBlock() for electra * chore: fix lint * fix: MAX_ATTESTATIONS_PER_GROUP_ELECTRA and address PR comments * chore: unit test aggregateConsolidation * Fix rebase mistake * Address my own comment :) --------- Co-authored-by: Navie Chan * Fix check-types * Address comments --------- Co-authored-by: Nazar Hussain Co-authored-by: tuyennhv --- packages/api/src/beacon/routes/beacon/pool.ts | 66 +++- packages/api/src/beacon/routes/events.ts | 8 +- packages/api/src/beacon/routes/validator.ts | 93 +++-- .../src/api/impl/beacon/pool/index.ts | 4 +- .../src/api/impl/validator/index.ts | 4 +- .../src/chain/blocks/importBlock.ts | 10 +- .../src/chain/errors/attestationError.ts | 12 +- .../opPools/aggregatedAttestationPool.ts | 349 ++++++++++++++---- .../src/chain/opPools/attestationPool.ts | 72 +++- .../beacon-node/src/chain/opPools/opPool.ts | 8 +- .../src/chain/validation/aggregateAndProof.ts | 33 +- .../src/chain/validation/attestation.ts | 50 ++- .../src/chain/validation/attesterSlashing.ts | 2 +- .../src/metrics/validatorMonitor.ts | 2 +- .../src/network/gossip/interface.ts | 10 +- .../beacon-node/src/network/gossip/topic.ts | 11 +- packages/beacon-node/src/network/interface.ts | 2 +- packages/beacon-node/src/network/network.ts | 2 +- .../src/network/processor/gossipHandlers.ts | 17 +- .../test/unit/api/impl/events/events.test.ts | 6 +- .../opPools/aggregatedAttestationPool.test.ts | 87 ++++- .../fork-choice/src/forkChoice/interface.ts | 2 +- .../src/signatureSets/attesterSlashings.ts | 6 +- .../src/signatureSets/index.ts | 6 +- packages/types/src/allForks/types.ts | 6 + .../validator/src/services/attestation.ts | 6 +- .../validator/src/services/validatorStore.ts | 37 +- packages/validator/src/validator.ts | 1 + .../test/unit/services/attestation.test.ts | 3 + 29 files changed, 724 insertions(+), 191 deletions(-) diff --git a/packages/api/src/beacon/routes/beacon/pool.ts b/packages/api/src/beacon/routes/beacon/pool.ts index f957390131fe..3c390aa41481 100644 --- a/packages/api/src/beacon/routes/beacon/pool.ts +++ b/packages/api/src/beacon/routes/beacon/pool.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {ValueOf} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; +import {ForkSeq} from "@lodestar/params"; import {phase0, capella, CommitteeIndex, Slot, ssz} from "@lodestar/types"; import {Schema, Endpoint, RouteDefinitions} from "../../../utils/index.js"; import { @@ -12,18 +13,24 @@ import { EmptyRequest, EmptyResponseCodec, EmptyResponseData, + WithVersion, } from "../../../utils/codecs.js"; +import {MetaHeader, VersionCodec, VersionMeta} from "../../../utils/metadata.js"; +import {toForkName} from "../../../utils/fork.js"; // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes -const AttestationListType = ArrayOf(ssz.phase0.Attestation); +const AttestationListTypePhase0 = ArrayOf(ssz.phase0.Attestation); +const AttestationListTypeElectra = ArrayOf(ssz.electra.Attestation); const AttesterSlashingListType = ArrayOf(ssz.phase0.AttesterSlashing); const ProposerSlashingListType = ArrayOf(ssz.phase0.ProposerSlashing); const SignedVoluntaryExitListType = ArrayOf(ssz.phase0.SignedVoluntaryExit); const SignedBLSToExecutionChangeListType = ArrayOf(ssz.capella.SignedBLSToExecutionChange); const SyncCommitteeMessageListType = ArrayOf(ssz.altair.SyncCommitteeMessage); -type AttestationList = ValueOf; +type AttestationListPhase0 = ValueOf; +type AttestationListElectra = ValueOf; +type AttestationList = AttestationListPhase0 | AttestationListElectra; type AttesterSlashingList = ValueOf; type ProposerSlashingList = ValueOf; type SignedVoluntaryExitList = ValueOf; @@ -40,7 +47,7 @@ export type Endpoints = { {slot?: Slot; committeeIndex?: CommitteeIndex}, {query: {slot?: number; committee_index?: number}}, AttestationList, - EmptyMeta + VersionMeta >; /** @@ -106,7 +113,7 @@ export type Endpoints = { submitPoolAttestations: Endpoint< "POST", {signedAttestations: AttestationList}, - {body: unknown}, + {body: unknown; headers: {[MetaHeader.Version]: string}}, EmptyResponseData, EmptyMeta >; @@ -172,7 +179,7 @@ export type Endpoints = { >; }; -export function getDefinitions(_config: ChainForkConfig): RouteDefinitions { +export function getDefinitions(config: ChainForkConfig): RouteDefinitions { return { getPoolAttestations: { url: "/eth/v1/beacon/pool/attestations", @@ -183,8 +190,10 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions + ForkSeq[fork] >= ForkSeq.electra ? AttestationListTypeElectra : AttestationListTypePhase0 + ), + meta: VersionCodec, }, }, getPoolAttesterSlashings: { @@ -227,12 +236,47 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions ({body: AttestationListType.toJson(signedAttestations)}), - parseReqJson: ({body}) => ({signedAttestations: AttestationListType.fromJson(body)}), - writeReqSsz: ({signedAttestations}) => ({body: AttestationListType.serialize(signedAttestations)}), - parseReqSsz: ({body}) => ({signedAttestations: AttestationListType.deserialize(body)}), + writeReqJson: ({signedAttestations}) => { + const fork = config.getForkName(signedAttestations[0].data.slot); + return { + body: + ForkSeq[fork] >= ForkSeq.electra + ? AttestationListTypeElectra.toJson(signedAttestations as AttestationListElectra) + : AttestationListTypePhase0.toJson(signedAttestations as AttestationListPhase0), + headers: {[MetaHeader.Version]: fork}, + }; + }, + parseReqJson: ({body, headers}) => { + const fork = toForkName(headers[MetaHeader.Version]); + return { + signedAttestations: + ForkSeq[fork] >= ForkSeq.electra + ? AttestationListTypeElectra.fromJson(body) + : AttestationListTypePhase0.fromJson(body), + }; + }, + writeReqSsz: ({signedAttestations}) => { + const fork = config.getForkName(signedAttestations[0].data.slot); + return { + body: + ForkSeq[fork] >= ForkSeq.electra + ? AttestationListTypeElectra.serialize(signedAttestations as AttestationListElectra) + : AttestationListTypePhase0.serialize(signedAttestations as AttestationListPhase0), + headers: {[MetaHeader.Version]: fork}, + }; + }, + parseReqSsz: ({body, headers}) => { + const fork = toForkName(headers[MetaHeader.Version]); + return { + signedAttestations: + ForkSeq[fork] >= ForkSeq.electra + ? AttestationListTypeElectra.deserialize(body) + : AttestationListTypePhase0.deserialize(body), + }; + }, schema: { body: Schema.ObjectArray, + headers: {[MetaHeader.Version]: Schema.String}, }, }, resp: EmptyResponseCodec, diff --git a/packages/api/src/beacon/routes/events.ts b/packages/api/src/beacon/routes/events.ts index 0b88175d7588..245615135d2f 100644 --- a/packages/api/src/beacon/routes/events.ts +++ b/packages/api/src/beacon/routes/events.ts @@ -91,10 +91,10 @@ export type EventData = { block: RootHex; executionOptimistic: boolean; }; - [EventType.attestation]: phase0.Attestation; + [EventType.attestation]: {version: ForkName; data: allForks.Attestation}; [EventType.voluntaryExit]: phase0.SignedVoluntaryExit; [EventType.proposerSlashing]: phase0.ProposerSlashing; - [EventType.attesterSlashing]: phase0.AttesterSlashing; + [EventType.attesterSlashing]: {version: ForkName; data: allForks.AttesterSlashing}; [EventType.blsToExecutionChange]: capella.SignedBLSToExecutionChange; [EventType.finalizedCheckpoint]: { block: RootHex; @@ -212,10 +212,10 @@ export function getTypeByEvent(): {[K in EventType]: TypeJson} { {jsonCase: "eth2"} ), - [EventType.attestation]: ssz.phase0.Attestation, + [EventType.attestation]: WithVersion((fork) => (ssz.allForks[fork] as allForks.AllForksSSZTypes).Attestation), [EventType.voluntaryExit]: ssz.phase0.SignedVoluntaryExit, [EventType.proposerSlashing]: ssz.phase0.ProposerSlashing, - [EventType.attesterSlashing]: ssz.phase0.AttesterSlashing, + [EventType.attesterSlashing]: WithVersion((fork) => ssz.allForks[fork].AttesterSlashing), [EventType.blsToExecutionChange]: ssz.capella.SignedBLSToExecutionChange, [EventType.finalizedCheckpoint]: new ContainerType( diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index 7f704edd542a..25ab6851e150 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {ContainerType, fromHexString, toHexString, Type, ValueOf} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; -import {isForkBlobs} from "@lodestar/params"; +import {isForkBlobs, ForkSeq} from "@lodestar/params"; import { allForks, altair, @@ -207,7 +207,8 @@ export const ValidatorIndicesType = ArrayOf(ssz.ValidatorIndex); export const AttesterDutyListType = ArrayOf(AttesterDutyType); export const ProposerDutyListType = ArrayOf(ProposerDutyType); export const SyncDutyListType = ArrayOf(SyncDutyType); -export const SignedAggregateAndProofListType = ArrayOf(ssz.phase0.SignedAggregateAndProof); +export const SignedAggregateAndProofListPhase0Type = ArrayOf(ssz.phase0.SignedAggregateAndProof); +export const SignedAggregateAndProofListElectaType = ArrayOf(ssz.electra.SignedAggregateAndProof); export const SignedContributionAndProofListType = ArrayOf(ssz.altair.SignedContributionAndProof); export const BeaconCommitteeSubscriptionListType = ArrayOf(BeaconCommitteeSubscriptionType); export const SyncCommitteeSubscriptionListType = ArrayOf(SyncCommitteeSubscriptionType); @@ -224,7 +225,9 @@ export type ProposerDuty = ValueOf; export type ProposerDutyList = ValueOf; export type SyncDuty = ValueOf; export type SyncDutyList = ValueOf; -export type SignedAggregateAndProofList = ValueOf; +export type SignedAggregateAndProofListPhase0 = ValueOf; +export type SignedAggregateAndProofListElecta = ValueOf; +export type SignedAggregateAndProofList = SignedAggregateAndProofListPhase0 | SignedAggregateAndProofListElecta; export type SignedContributionAndProofList = ValueOf; export type BeaconCommitteeSubscription = ValueOf; export type BeaconCommitteeSubscriptionList = ValueOf; @@ -424,10 +427,11 @@ export type Endpoints = { /** HashTreeRoot of AttestationData that validator want's aggregated */ attestationDataRoot: Root; slot: Slot; + index: number; }, - {query: {attestation_data_root: string; slot: number}}, - phase0.Attestation, - EmptyMeta + {query: {attestation_data_root: string; slot: number; index: number}}, + allForks.Attestation, + VersionMeta >; /** @@ -437,7 +441,7 @@ export type Endpoints = { publishAggregateAndProofs: Endpoint< "POST", {signedAggregateAndProofs: SignedAggregateAndProofList}, - {body: unknown}, + {body: unknown; headers: {[MetaHeader.Version]: string}}, EmptyResponseData, EmptyMeta >; @@ -554,7 +558,7 @@ export type Endpoints = { >; }; -export function getDefinitions(_config: ChainForkConfig): RouteDefinitions { +export function getDefinitions(config: ChainForkConfig): RouteDefinitions { return { getAttesterDuties: { url: "/eth/v1/validator/duties/attester/{epoch}", @@ -846,33 +850,78 @@ export function getDefinitions(_config: ChainForkConfig): RouteDefinitions ({ - query: {attestation_data_root: toHexString(attestationDataRoot), slot}, + writeReq: ({attestationDataRoot, slot, index}) => ({ + query: {attestation_data_root: toHexString(attestationDataRoot), slot, index}, + }), + parseReq: ({query}) => ({ + attestationDataRoot: fromHexString(query.attestation_data_root), + slot: query.slot, + index: query.slot, }), - parseReq: ({query}) => ({attestationDataRoot: fromHexString(query.attestation_data_root), slot: query.slot}), schema: { - query: {attestation_data_root: Schema.StringRequired, slot: Schema.UintRequired}, + query: {attestation_data_root: Schema.StringRequired, slot: Schema.UintRequired, index: Schema.UintRequired}, }, }, resp: { - data: ssz.phase0.Attestation, - meta: EmptyMetaCodec, + data: WithVersion((fork) => + ForkSeq[fork] >= ForkSeq.electra ? ssz.electra.Attestation : ssz.phase0.Attestation + ), + meta: VersionCodec, }, }, publishAggregateAndProofs: { url: "/eth/v1/validator/aggregate_and_proofs", method: "POST", req: { - writeReqJson: ({signedAggregateAndProofs}) => ({ - body: SignedAggregateAndProofListType.toJson(signedAggregateAndProofs), - }), - parseReqJson: ({body}) => ({signedAggregateAndProofs: SignedAggregateAndProofListType.fromJson(body)}), - writeReqSsz: ({signedAggregateAndProofs}) => ({ - body: SignedAggregateAndProofListType.serialize(signedAggregateAndProofs), - }), - parseReqSsz: ({body}) => ({signedAggregateAndProofs: SignedAggregateAndProofListType.deserialize(body)}), + writeReqJson: ({signedAggregateAndProofs}) => { + const fork = config.getForkName(signedAggregateAndProofs[0].message.aggregate.data.slot); + return { + body: + ForkSeq[fork] >= ForkSeq.electra + ? SignedAggregateAndProofListElectaType.toJson( + signedAggregateAndProofs as SignedAggregateAndProofListElecta + ) + : SignedAggregateAndProofListPhase0Type.toJson( + signedAggregateAndProofs as SignedAggregateAndProofListPhase0 + ), + headers: {[MetaHeader.Version]: fork}, + }; + }, + parseReqJson: ({body, headers}) => { + const fork = toForkName(headers[MetaHeader.Version]); + return { + signedAggregateAndProofs: + ForkSeq[fork] >= ForkSeq.electra + ? SignedAggregateAndProofListElectaType.fromJson(body) + : SignedAggregateAndProofListPhase0Type.fromJson(body), + }; + }, + writeReqSsz: ({signedAggregateAndProofs}) => { + const fork = config.getForkName(signedAggregateAndProofs[0].message.aggregate.data.slot); + return { + body: + ForkSeq[fork] >= ForkSeq.electra + ? SignedAggregateAndProofListElectaType.serialize( + signedAggregateAndProofs as SignedAggregateAndProofListElecta + ) + : SignedAggregateAndProofListPhase0Type.serialize( + signedAggregateAndProofs as SignedAggregateAndProofListPhase0 + ), + headers: {[MetaHeader.Version]: fork}, + }; + }, + parseReqSsz: ({body, headers}) => { + const fork = toForkName(headers[MetaHeader.Version]); + return { + signedAggregateAndProofs: + ForkSeq[fork] >= ForkSeq.electra + ? SignedAggregateAndProofListElectaType.deserialize(body) + : SignedAggregateAndProofListPhase0Type.deserialize(body), + }; + }, schema: { body: Schema.ObjectArray, + headers: {[MetaHeader.Version]: Schema.String}, }, }, resp: EmptyResponseCodec, diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index 8372b84db3b1..77f6b24f28af 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -1,7 +1,7 @@ import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; import {Epoch, ssz} from "@lodestar/types"; -import {SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar/params"; +import {ForkName, SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar/params"; import {validateApiAttestation} from "../../../../chain/validation/index.js"; import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js"; import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js"; @@ -78,7 +78,7 @@ export function getBeaconPoolApi({ metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome}); } - chain.emitter.emit(routes.events.EventType.attestation, attestation); + chain.emitter.emit(routes.events.EventType.attestation, {data: attestation, version: ForkName.phase0}); const sentPeers = await network.publishBeaconAttestation(attestation, subnet); metrics?.onPoolSubmitUnaggregatedAttestation(seenTimestampSec, indexedAttestation, subnet, sentPeers); diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 2c0958de5d5a..7652a67daeb1 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -819,6 +819,7 @@ export function getValidatorApi({ const attEpoch = computeEpochAtSlot(slot); const headBlockRootHex = chain.forkChoice.getHead().blockRoot; const headBlockRoot = fromHex(headBlockRootHex); + const fork = config.getForkSeq(slot); const beaconBlockRoot = slot >= headSlot @@ -850,7 +851,7 @@ export function getValidatorApi({ return { data: { slot, - index: committeeIndex, + index: fork >= ForkSeq.electra ? 0 : committeeIndex, beaconBlockRoot, source: attEpochState.currentJustifiedCheckpoint, target: {epoch: attEpoch, root: targetRoot}, @@ -1087,6 +1088,7 @@ export function getValidatorApi({ return { data: aggregate, + version: config.getForkName(slot), }; }, diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 71645ae6b4f8..2e777d8e9a27 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -428,12 +428,18 @@ export async function importBlock( } if (this.emitter.listenerCount(routes.events.EventType.attestation)) { for (const attestation of block.message.body.attestations) { - this.emitter.emit(routes.events.EventType.attestation, attestation); + this.emitter.emit(routes.events.EventType.attestation, { + version: this.config.getForkName(blockSlot), + data: attestation, + }); } } if (this.emitter.listenerCount(routes.events.EventType.attesterSlashing)) { for (const attesterSlashing of block.message.body.attesterSlashings) { - this.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing); + this.emitter.emit(routes.events.EventType.attesterSlashing, { + version: this.config.getForkName(blockSlot), + data: attesterSlashing, + }); } } if (this.emitter.listenerCount(routes.events.EventType.proposerSlashing)) { diff --git a/packages/beacon-node/src/chain/errors/attestationError.ts b/packages/beacon-node/src/chain/errors/attestationError.ts index 8e0dc925f32e..b40af0489911 100644 --- a/packages/beacon-node/src/chain/errors/attestationError.ts +++ b/packages/beacon-node/src/chain/errors/attestationError.ts @@ -127,6 +127,14 @@ export enum AttestationErrorCode { INVALID_SERIALIZED_BYTES = "ATTESTATION_ERROR_INVALID_SERIALIZED_BYTES", /** Too many skipped slots. */ TOO_MANY_SKIPPED_SLOTS = "ATTESTATION_ERROR_TOO_MANY_SKIPPED_SLOTS", + /** + * Electra: The aggregated attestation doesn't have only one committee bit set. + */ + NOT_EXACTLY_ONE_COMMITTEE_BIT_SET = "ATTESTATION_ERROR_NOT_EXACTLY_ONE_COMMITTEE_BIT_SET", + /** + * Electra: Invalid attestationData index: is non-zero + */ + NON_ZERO_ATTESTATION_DATA_INDEX = "ATTESTATION_ERROR_NON_ZERO_ATTESTATION_DATA_INDEX", } export type AttestationErrorType = @@ -160,7 +168,9 @@ export type AttestationErrorType = | {code: AttestationErrorCode.INVALID_AGGREGATOR} | {code: AttestationErrorCode.INVALID_INDEXED_ATTESTATION} | {code: AttestationErrorCode.INVALID_SERIALIZED_BYTES} - | {code: AttestationErrorCode.TOO_MANY_SKIPPED_SLOTS; headBlockSlot: Slot; attestationSlot: Slot}; + | {code: AttestationErrorCode.TOO_MANY_SKIPPED_SLOTS; headBlockSlot: Slot; attestationSlot: Slot} + | {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET} + | {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}; export class AttestationError extends GossipActionError { getMetadata(): Record { diff --git a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts index c94e5d81e823..d6cceb9572ae 100644 --- a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts @@ -1,7 +1,26 @@ import bls from "@chainsafe/bls"; -import {toHexString} from "@chainsafe/ssz"; -import {ForkName, ForkSeq, MAX_ATTESTATIONS, MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH} from "@lodestar/params"; -import {phase0, Epoch, Slot, ssz, ValidatorIndex, RootHex} from "@lodestar/types"; +import {Signature} from "@chainsafe/bls/types"; +import {BitArray, toHexString} from "@chainsafe/ssz"; +import { + ForkName, + ForkSeq, + MAX_ATTESTATIONS, + MAX_ATTESTATIONS_ELECTRA, + MAX_COMMITTEES_PER_SLOT, + MIN_ATTESTATION_INCLUSION_DELAY, + SLOTS_PER_EPOCH, +} from "@lodestar/params"; +import { + phase0, + Epoch, + Slot, + ssz, + ValidatorIndex, + RootHex, + allForks, + electra, + isElectraAttestation, +} from "@lodestar/types"; import { CachedBeaconStateAllForks, CachedBeaconStatePhase0, @@ -20,13 +39,24 @@ type DataRootHex = string; type CommitteeIndex = number; -type AttestationWithScore = {attestation: phase0.Attestation; score: number}; +// for pre-electra +type AttestationWithScore = {attestation: allForks.Attestation; score: number}; +/** + * for electra, this is to consolidate aggregated attestations of the same attestation data into a single attestation to be included in block + * note that this is local definition in this file and it's NOT validator consolidation + */ +export type AttestationsConsolidation = { + byCommittee: Map; + attData: phase0.AttestationData; + totalNotSeenCount: number; + score: number; +}; /** - * This function returns not seen participation for a given epoch and committee. + * This function returns not seen participation for a given epoch and slot and committe index. * Return null if all validators are seen or no info to check. */ -type GetNotSeenValidatorsFn = (epoch: Epoch, committee: Uint32Array) => Set | null; +type GetNotSeenValidatorsFn = (epoch: Epoch, slot: Slot, committeeIndex: number) => Set | null; type ValidateAttestationDataFn = (attData: phase0.AttestationData) => boolean; @@ -39,14 +69,21 @@ type ValidateAttestationDataFn = (attData: phase0.AttestationData) => boolean; const MAX_RETAINED_ATTESTATIONS_PER_GROUP = 4; /** - * On mainnet, each slot has 64 committees, and each block has 128 attestations max so in average + * Pre-electra, each slot has 64 committees, and each block has 128 attestations max so in average * we get 2 attestation per groups. * Starting from Jan 2024, we have a performance issue getting attestations for a block. Based on the - * fact that lot of groups will have only 1 attestation since it's full of participation increase this number + * fact that lot of groups will have only 1 full participation attestation, increase this number * a bit higher than average. This also help decrease number of slots to search for attestations. */ const MAX_ATTESTATIONS_PER_GROUP = 3; +/** + * For electra, each block has up to 8 aggregated attestations, assuming there are 3 for the "best" + * attestation data, there are still 5 for other attestation data so this constant is still good. + * We should separate to 2 constant based on conditions of different networks + */ +const MAX_ATTESTATIONS_PER_GROUP_ELECTRA = 3; + /** * Maintain a pool of aggregated attestations. Attestations can be retrieved for inclusion in a block * or api. The returned attestations are aggregated to maximise the number of validators that can be @@ -54,20 +91,25 @@ const MAX_ATTESTATIONS_PER_GROUP = 3; * Note that we want to remove attestations with attesters that were included in the chain. */ export class AggregatedAttestationPool { - private readonly attestationGroupByDataHashByIndexBySlot = new MapDef< + /** + * post electra, different committees could have the same AttData and we have to consolidate attestations of the same + * data to be included in block, so we should group by data before index + * // TODO: make sure it does not affect performance for pre electra forks + */ + private readonly attestationGroupByIndexByDataHexBySlot = new MapDef< Slot, - Map> - >(() => new Map>()); + Map> + >(() => new Map>()); private lowestPermissibleSlot = 0; /** For metrics to track size of the pool */ getAttestationCount(): {attestationCount: number; attestationDataCount: number} { let attestationCount = 0; let attestationDataCount = 0; - for (const attestationGroupByDataByIndex of this.attestationGroupByDataHashByIndexBySlot.values()) { - for (const attestationGroupByData of attestationGroupByDataByIndex.values()) { - attestationDataCount += attestationGroupByData.size; - for (const attestationGroup of attestationGroupByData.values()) { + for (const attestationGroupByIndexByDataHex of this.attestationGroupByIndexByDataHexBySlot.values()) { + for (const attestationGroupByIndex of attestationGroupByIndexByDataHex.values()) { + attestationDataCount += attestationGroupByIndex.size; + for (const attestationGroup of attestationGroupByIndex.values()) { attestationCount += attestationGroup.getAttestationCount(); } } @@ -76,7 +118,7 @@ export class AggregatedAttestationPool { } add( - attestation: phase0.Attestation, + attestation: allForks.Attestation, dataRootHex: RootHex, attestingIndicesCount: number, committee: Uint32Array @@ -89,16 +131,24 @@ export class AggregatedAttestationPool { return InsertOutcome.Old; } - const attestationGroupByDataHashByIndex = this.attestationGroupByDataHashByIndexBySlot.getOrDefault(slot); - let attestationGroupByDataHash = attestationGroupByDataHashByIndex.get(attestation.data.index); - if (!attestationGroupByDataHash) { - attestationGroupByDataHash = new Map(); - attestationGroupByDataHashByIndex.set(attestation.data.index, attestationGroupByDataHash); + const attestationGroupByIndexByDataHash = this.attestationGroupByIndexByDataHexBySlot.getOrDefault(slot); + let attestationGroupByIndex = attestationGroupByIndexByDataHash.get(dataRootHex); + if (!attestationGroupByIndex) { + attestationGroupByIndex = new Map(); + attestationGroupByIndexByDataHash.set(dataRootHex, attestationGroupByIndex); + } + const committeeIndex = isElectraAttestation(attestation) + ? // this attestation is added to pool after validation + attestation.committeeBits.getSingleTrueBit() + : attestation.data.index; + if (committeeIndex === null) { + // this should not happen because attestation should be validated before reaching this + throw Error(`Invalid attestation slot=${slot} committeeIndex=${committeeIndex}`); } - let attestationGroup = attestationGroupByDataHash.get(dataRootHex); + let attestationGroup = attestationGroupByIndex.get(committeeIndex); if (!attestationGroup) { attestationGroup = new MatchingDataAttestationGroup(committee, attestation.data); - attestationGroupByDataHash.set(dataRootHex, attestationGroup); + attestationGroupByIndex.set(committeeIndex, attestationGroup); } return attestationGroup.add({ @@ -110,14 +160,25 @@ export class AggregatedAttestationPool { /** Remove attestations which are too old to be included in a block. */ prune(clockSlot: Slot): void { // Only retain SLOTS_PER_EPOCH slots - pruneBySlot(this.attestationGroupByDataHashByIndexBySlot, clockSlot, SLOTS_PER_EPOCH); + pruneBySlot(this.attestationGroupByIndexByDataHexBySlot, clockSlot, SLOTS_PER_EPOCH); this.lowestPermissibleSlot = Math.max(clockSlot - SLOTS_PER_EPOCH, 0); } + getAttestationsForBlock( + fork: ForkName, + forkChoice: IForkChoice, + state: CachedBeaconStateAllForks + ): allForks.Attestation[] { + const forkSeq = ForkSeq[fork]; + return forkSeq >= ForkSeq.electra + ? this.getAttestationsForBlockElectra(fork, forkChoice, state) + : this.getAttestationsForBlockPreElectra(fork, forkChoice, state); + } + /** - * Get attestations to be included in a block. Returns $MAX_ATTESTATIONS items + * Get attestations to be included in a block pre-electra. Returns up to $MAX_ATTESTATIONS items */ - getAttestationsForBlock( + getAttestationsForBlockPreElectra( fork: ForkName, forkChoice: IForkChoice, state: CachedBeaconStateAllForks @@ -131,14 +192,14 @@ export class AggregatedAttestationPool { const attestationsByScore: AttestationWithScore[] = []; - const slots = Array.from(this.attestationGroupByDataHashByIndexBySlot.keys()).sort((a, b) => b - a); + const slots = Array.from(this.attestationGroupByIndexByDataHexBySlot.keys()).sort((a, b) => b - a); let minScore = Number.MAX_SAFE_INTEGER; let slotCount = 0; slot: for (const slot of slots) { slotCount++; - const attestationGroupByDataHashByIndex = this.attestationGroupByDataHashByIndexBySlot.get(slot); + const attestationGroupByIndexByDataHash = this.attestationGroupByIndexByDataHexBySlot.get(slot); // should not happen - if (!attestationGroupByDataHashByIndex) { + if (!attestationGroupByIndexByDataHash) { throw Error(`No aggregated attestation pool for slot=${slot}`); } @@ -159,35 +220,25 @@ export class AggregatedAttestationPool { } const slotDelta = stateSlot - slot; - const shuffling = state.epochCtx.getShufflingAtEpoch(epoch); - const slotCommittees = shuffling.committees[slot % SLOTS_PER_EPOCH]; - for (const [committeeIndex, attestationGroupByData] of attestationGroupByDataHashByIndex.entries()) { - // all attestations will be validated against the state in next step so we can get committee from the state - // this is an improvement to save the notSeenValidatorsFn call for the same slot/index instead of the same attestation data - if (committeeIndex > slotCommittees.length) { - // invalid index, should not happen - continue; - } - - const committee = slotCommittees[committeeIndex]; - const notSeenAttestingIndices = notSeenValidatorsFn(epoch, committee); - if (notSeenAttestingIndices === null || notSeenAttestingIndices.size === 0) { - continue; - } + for (const attestationGroupByIndex of attestationGroupByIndexByDataHash.values()) { + for (const [committeeIndex, attestationGroup] of attestationGroupByIndex.entries()) { + const notSeenAttestingIndices = notSeenValidatorsFn(epoch, slot, committeeIndex); + if (notSeenAttestingIndices === null || notSeenAttestingIndices.size === 0) { + continue; + } - if ( - slotCount > 2 && - attestationsByScore.length >= MAX_ATTESTATIONS && - notSeenAttestingIndices.size / slotDelta < minScore - ) { - // after 2 slots, there are a good chance that we have 2 * MAX_ATTESTATIONS attestations and break the for loop early - // if not, we may have to scan all slots in the pool - // if we have enough attestations and the max possible score is lower than scores of `attestationsByScore`, we should skip - // otherwise it takes time to check attestation, add it and remove it later after the sort by score - continue; - } + if ( + slotCount > 2 && + attestationsByScore.length >= MAX_ATTESTATIONS && + notSeenAttestingIndices.size / slotDelta < minScore + ) { + // after 2 slots, there are a good chance that we have 2 * MAX_ATTESTATIONS attestations and break the for loop early + // if not, we may have to scan all slots in the pool + // if we have enough attestations and the max possible score is lower than scores of `attestationsByScore`, we should skip + // otherwise it takes time to check attestation, add it and remove it later after the sort by score + continue; + } - for (const attestationGroup of attestationGroupByData.values()) { if (!validateAttestationDataFn(attestationGroup.data)) { continue; } @@ -200,6 +251,7 @@ export class AggregatedAttestationPool { // IF they have to be validated, do it only with one attestation per group since same data // The committeeCountPerSlot can be precomputed once per slot for (const {attestation, notSeenAttesterCount} of attestationGroup.getAttestationsForBlock( + fork, notSeenAttestingIndices )) { const score = notSeenAttesterCount / slotDelta; @@ -232,23 +284,134 @@ export class AggregatedAttestationPool { return attestationsForBlock; } + /** + * Get attestations to be included in an electra block. Returns up to $MAX_ATTESTATIONS_ELECTRA items + */ + getAttestationsForBlockElectra( + fork: ForkName, + forkChoice: IForkChoice, + state: CachedBeaconStateAllForks + ): electra.Attestation[] { + const stateSlot = state.slot; + const stateEpoch = state.epochCtx.epoch; + const statePrevEpoch = stateEpoch - 1; + + const notSeenValidatorsFn = getNotSeenValidatorsFn(state); + const validateAttestationDataFn = getValidateAttestationDataFn(forkChoice, state); + + const slots = Array.from(this.attestationGroupByIndexByDataHexBySlot.keys()).sort((a, b) => b - a); + const consolidations: AttestationsConsolidation[] = []; + let minScore = Number.MAX_SAFE_INTEGER; + let slotCount = 0; + slot: for (const slot of slots) { + slotCount++; + const attestationGroupByIndexByDataHash = this.attestationGroupByIndexByDataHexBySlot.get(slot); + // should not happen + if (!attestationGroupByIndexByDataHash) { + throw Error(`No aggregated attestation pool for slot=${slot}`); + } + + const epoch = computeEpochAtSlot(slot); + // validateAttestation condition: Attestation target epoch not in previous or current epoch + if (!(epoch === stateEpoch || epoch === statePrevEpoch)) { + continue; // Invalid attestations + } + // validateAttestation condition: Attestation slot not within inclusion window + if (!(slot + MIN_ATTESTATION_INCLUSION_DELAY <= stateSlot)) { + continue; // Invalid attestations + } + + const slotDelta = stateSlot - slot; + // CommitteeIndex 0 1 2 ... Consolidation + // Attestations att00 --- att10 --- att20 --- 0 (att 00 10 20) + // att01 --- - --- att21 --- 1 (att 01 __ 21) + // - --- - --- att22 --- 2 (att __ __ 22) + for (const attestationGroupByIndex of attestationGroupByIndexByDataHash.values()) { + // sameAttDataCons could be up to MAX_ATTESTATIONS_PER_GROUP_ELECTRA + const sameAttDataCons: AttestationsConsolidation[] = []; + for (const [committeeIndex, attestationGroup] of attestationGroupByIndex.entries()) { + const notSeenAttestingIndices = notSeenValidatorsFn(epoch, slot, committeeIndex); + if (notSeenAttestingIndices === null || notSeenAttestingIndices.size === 0) { + continue; + } + + if ( + slotCount > 2 && + consolidations.length >= MAX_ATTESTATIONS_ELECTRA && + notSeenAttestingIndices.size / slotDelta < minScore + ) { + // after 2 slots, there are a good chance that we have 2 * MAX_ATTESTATIONS_ELECTRA attestations and break the for loop early + // if not, we may have to scan all slots in the pool + // if we have enough attestations and the max possible score is lower than scores of `attestationsByScore`, we should skip + // otherwise it takes time to check attestation, add it and remove it later after the sort by score + continue; + } + + if (!validateAttestationDataFn(attestationGroup.data)) { + continue; + } + + // TODO: Is it necessary to validateAttestation for: + // - Attestation committee index not within current committee count + // - Attestation aggregation bits length does not match committee length + // + // These properties should not change after being validate in gossip + // IF they have to be validated, do it only with one attestation per group since same data + // The committeeCountPerSlot can be precomputed once per slot + for (const [i, attestationNonParticipation] of attestationGroup + .getAttestationsForBlock(fork, notSeenAttestingIndices) + .entries()) { + if (sameAttDataCons[i] === undefined) { + sameAttDataCons[i] = { + byCommittee: new Map(), + attData: attestationNonParticipation.attestation.data, + totalNotSeenCount: 0, + // only update score after we have full data + score: 0, + }; + } + sameAttDataCons[i].byCommittee.set(committeeIndex, attestationNonParticipation); + sameAttDataCons[i].totalNotSeenCount += attestationNonParticipation.notSeenAttesterCount; + } + for (const consolidation of sameAttDataCons) { + const score = consolidation.totalNotSeenCount / slotDelta; + if (score < minScore) { + minScore = score; + } + consolidations.push({...consolidation, score}); + // Stop accumulating attestations there are enough that may have good scoring + if (consolidations.length >= MAX_ATTESTATIONS_ELECTRA * 2) { + break slot; + } + } + } + } + } + + const sortedConsolidationsByScore = consolidations + .sort((a, b) => b.score - a.score) + .slice(0, MAX_ATTESTATIONS_ELECTRA); + // on chain aggregation is expensive, only do it after all + return sortedConsolidationsByScore.map(aggregateConsolidation); + } + /** * Get all attestations optionally filtered by `attestation.data.slot` * @param bySlot slot to filter, `bySlot === attestation.data.slot` */ - getAll(bySlot?: Slot): phase0.Attestation[] { - let attestationGroupsArr: Map[]; + getAll(bySlot?: Slot): allForks.Attestation[] { + let attestationGroupsArr: Map[]; if (bySlot === undefined) { - attestationGroupsArr = Array.from(this.attestationGroupByDataHashByIndexBySlot.values()).flatMap((byIndex) => + attestationGroupsArr = Array.from(this.attestationGroupByIndexByDataHexBySlot.values()).flatMap((byIndex) => Array.from(byIndex.values()) ); } else { - const attestationGroupsByIndex = this.attestationGroupByDataHashByIndexBySlot.get(bySlot); + const attestationGroupsByIndex = this.attestationGroupByIndexByDataHexBySlot.get(bySlot); if (!attestationGroupsByIndex) throw Error(`No attestations for slot ${bySlot}`); attestationGroupsArr = Array.from(attestationGroupsByIndex.values()); } - const attestations: phase0.Attestation[] = []; + const attestations: allForks.Attestation[] = []; for (const attestationGroups of attestationGroupsArr) { for (const attestationGroup of attestationGroups.values()) { attestations.push(...attestationGroup.getAttestations()); @@ -259,12 +422,12 @@ export class AggregatedAttestationPool { } interface AttestationWithIndex { - attestation: phase0.Attestation; + attestation: allForks.Attestation; trueBitsCount: number; } type AttestationNonParticipant = { - attestation: phase0.Attestation; + attestation: allForks.Attestation; // this is <= attestingIndices.count since some attesters may be seen by the chain // this is only updated and used in removeBySeenValidators function notSeenAttesterCount: number; @@ -346,8 +509,9 @@ export class MatchingDataAttestationGroup { * @param notSeenAttestingIndices not seen attestting indices, i.e. indices in the same committee * @returns an array of AttestationNonParticipant */ - getAttestationsForBlock(notSeenAttestingIndices: Set): AttestationNonParticipant[] { + getAttestationsForBlock(fork: ForkName, notSeenAttestingIndices: Set): AttestationNonParticipant[] { const attestations: AttestationNonParticipant[] = []; + const forkSeq = ForkSeq[fork]; for (const {attestation} of this.attestations) { let notSeenAttesterCount = 0; const {aggregationBits} = attestation; @@ -357,22 +521,22 @@ export class MatchingDataAttestationGroup { } } - if (notSeenAttesterCount > 0) { + // if fork >= electra, should return electra-only attestations + if (notSeenAttesterCount > 0 && (forkSeq < ForkSeq.electra || isElectraAttestation(attestation))) { attestations.push({attestation, notSeenAttesterCount}); } } - if (attestations.length <= MAX_ATTESTATIONS_PER_GROUP) { + const maxAttestation = forkSeq >= ForkSeq.electra ? MAX_ATTESTATIONS_PER_GROUP_ELECTRA : MAX_ATTESTATIONS_PER_GROUP; + if (attestations.length <= maxAttestation) { return attestations; } else { - return attestations - .sort((a, b) => b.notSeenAttesterCount - a.notSeenAttesterCount) - .slice(0, MAX_ATTESTATIONS_PER_GROUP); + return attestations.sort((a, b) => b.notSeenAttesterCount - a.notSeenAttesterCount).slice(0, maxAttestation); } } /** Get attestations for API. */ - getAttestations(): phase0.Attestation[] { + getAttestations(): allForks.Attestation[] { return this.attestations.map((attestation) => attestation.attestation); } } @@ -386,6 +550,34 @@ export function aggregateInto(attestation1: AttestationWithIndex, attestation2: attestation1.attestation.signature = bls.Signature.aggregate([signature1, signature2]).toBytes(); } +/** + * Electra and after: Block proposer consolidates attestations with the same + * attestation data from different committee into a single attestation + * https://github.com/ethereum/consensus-specs/blob/aba6345776aa876dad368cab27fbbb23fae20455/specs/_features/eip7549/validator.md?plain=1#L39 + */ +export function aggregateConsolidation({byCommittee, attData}: AttestationsConsolidation): electra.Attestation { + const committeeBits = BitArray.fromBitLen(MAX_COMMITTEES_PER_SLOT); + // TODO: can we improve this? + let aggregationBits: boolean[] = []; + const signatures: Signature[] = []; + const sortedCommittees = Array.from(byCommittee.keys()).sort((a, b) => a - b); + for (const committeeIndex of sortedCommittees) { + const attestationNonParticipation = byCommittee.get(committeeIndex); + if (attestationNonParticipation !== undefined) { + const {attestation} = attestationNonParticipation; + committeeBits.set(committeeIndex, true); + aggregationBits = [...aggregationBits, ...attestation.aggregationBits.toBoolArray()]; + signatures.push(signatureFromBytesNoCheck(attestation.signature)); + } + } + return { + aggregationBits: BitArray.fromBoolArray(aggregationBits), + data: attData, + committeeBits, + signature: bls.Signature.aggregate(signatures).toBytes(), + }; +} + /** * Pre-compute participation from a CachedBeaconStateAllForks, for use to check if an attestation's committee * has already attested or not. @@ -408,12 +600,13 @@ export function getNotSeenValidatorsFn(state: CachedBeaconStateAllForks): GetNot state ); - return (epoch: Epoch, committee: Uint32Array) => { + return (epoch: Epoch, slot: Slot, committeeIndex: number) => { const participants = epoch === stateEpoch ? currentEpochParticipants : epoch === stateEpoch - 1 ? previousEpochParticipants : null; if (participants === null) { return null; } + const committee = state.epochCtx.getBeaconCommittee(slot, committeeIndex); const notSeenAttestingIndices = new Set(); for (const [i, validatorIndex] of committee.entries()) { @@ -435,22 +628,32 @@ export function getNotSeenValidatorsFn(state: CachedBeaconStateAllForks): GetNot const previousParticipation = altairState.previousEpochParticipation.getAll(); const currentParticipation = altairState.currentEpochParticipation.getAll(); const stateEpoch = computeEpochAtSlot(state.slot); + // this function could be called multiple times with same slot + committeeIndex + const cachedNotSeenValidators = new Map>(); - return (epoch: Epoch, committee: Uint32Array) => { + return (epoch: Epoch, slot: Slot, committeeIndex: number) => { const participationStatus = epoch === stateEpoch ? currentParticipation : epoch === stateEpoch - 1 ? previousParticipation : null; if (participationStatus === null) { return null; } + const cacheKey = slot + "_" + committeeIndex; + let notSeenAttestingIndices = cachedNotSeenValidators.get(cacheKey); + if (notSeenAttestingIndices != null) { + // if all validators are seen then return null, we don't need to check for any attestations of same committee again + return notSeenAttestingIndices.size === 0 ? null : notSeenAttestingIndices; + } - const notSeenAttestingIndices = new Set(); + const committee = state.epochCtx.getBeaconCommittee(slot, committeeIndex); + notSeenAttestingIndices = new Set(); for (const [i, validatorIndex] of committee.entries()) { // no need to check flagIsTimelySource as if validator is not seen, it's participation status is 0 if (participationStatus[validatorIndex] === 0) { notSeenAttestingIndices.add(i); } } + cachedNotSeenValidators.set(cacheKey, notSeenAttestingIndices); // if all validators are seen then return null, we don't need to check for any attestations of same committee again return notSeenAttestingIndices.size === 0 ? null : notSeenAttestingIndices; }; diff --git a/packages/beacon-node/src/chain/opPools/attestationPool.ts b/packages/beacon-node/src/chain/opPools/attestationPool.ts index 804d8798cbc2..38e910753440 100644 --- a/packages/beacon-node/src/chain/opPools/attestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/attestationPool.ts @@ -1,7 +1,7 @@ import {PointFormat, Signature} from "@chainsafe/bls/types"; import bls from "@chainsafe/bls"; import {BitArray} from "@chainsafe/ssz"; -import {phase0, Slot, RootHex} from "@lodestar/types"; +import {Slot, RootHex, allForks} from "@lodestar/types"; import {MapDef} from "@lodestar/utils"; import {IClock} from "../../util/clock.js"; import {InsertOutcome, OpPoolError, OpPoolErrorCode} from "./types.js"; @@ -23,12 +23,16 @@ const SLOTS_RETAINED = 3; */ const MAX_ATTESTATIONS_PER_SLOT = 16_384; -type AggregateFast = { - data: phase0.Attestation["data"]; +type AggregateFastPhase0 = { + data: allForks.Attestation["data"]; aggregationBits: BitArray; signature: Signature; }; +type AggregateFastElectra = AggregateFastPhase0 & {committeeBits: BitArray}; + +type AggregateFast = AggregateFastPhase0 | AggregateFastElectra; + /** Hex string of DataRoot `TODO` */ type DataRootHex = string; @@ -93,7 +97,7 @@ export class AttestationPool { * - Valid committeeIndex * - Valid data */ - add(attestation: phase0.Attestation, attDataRootHex: RootHex): InsertOutcome { + add(attestation: allForks.Attestation, attDataRootHex: RootHex): InsertOutcome { const slot = attestation.data.slot; const lowestPermissibleSlot = this.lowestPermissibleSlot; @@ -128,7 +132,7 @@ export class AttestationPool { /** * For validator API to get an aggregate */ - getAggregate(slot: Slot, dataRootHex: RootHex): phase0.Attestation | null { + getAggregate(slot: Slot, dataRootHex: RootHex): allForks.Attestation | null { const aggregate = this.attestationByRootBySlot.get(slot)?.get(dataRootHex); if (!aggregate) { // TODO: Add metric for missing aggregates @@ -152,8 +156,8 @@ export class AttestationPool { * Get all attestations optionally filtered by `attestation.data.slot` * @param bySlot slot to filter, `bySlot === attestation.data.slot` */ - getAll(bySlot?: Slot): phase0.Attestation[] { - const attestations: phase0.Attestation[] = []; + getAll(bySlot?: Slot): allForks.Attestation[] { + const attestations: allForks.Attestation[] = []; const aggregateByRoots = bySlot === undefined @@ -178,7 +182,7 @@ export class AttestationPool { /** * Aggregate a new contribution into `aggregate` mutating it */ -function aggregateAttestationInto(aggregate: AggregateFast, attestation: phase0.Attestation): InsertOutcome { +function aggregateAttestationInto(aggregate: AggregateFast, attestation: allForks.Attestation): InsertOutcome { const bitIndex = attestation.aggregationBits.getSingleTrueBit(); // Should never happen, attestations are verified against this exact condition before @@ -186,6 +190,26 @@ function aggregateAttestationInto(aggregate: AggregateFast, attestation: phase0. throw Error("Invalid attestation not exactly one bit set"); } + if ("committeeBits" in attestation && !("committeeBits" in aggregate)) { + throw Error("Attempt to aggregate electra attestation into phase0 attestation"); + } + + if (!("committeeBits" in attestation) && "committeeBits" in aggregate) { + throw Error("Attempt to aggregate phase0 attestation into electra attestation"); + } + + if ("committeeBits" in attestation) { + // We assume attestation.committeeBits should already be validated in api and gossip handler and should be non-null + const attestationCommitteeIndex = attestation.committeeBits.getSingleTrueBit(); + const aggregateCommitteeIndex = (aggregate as AggregateFastElectra).committeeBits.getSingleTrueBit(); + + if (attestationCommitteeIndex !== aggregateCommitteeIndex) { + throw Error( + `Committee index mismatched: attestation ${attestationCommitteeIndex} aggregate ${aggregateCommitteeIndex}` + ); + } + } + if (aggregate.aggregationBits.get(bitIndex) === true) { return InsertOutcome.AlreadyKnown; } @@ -201,7 +225,16 @@ function aggregateAttestationInto(aggregate: AggregateFast, attestation: phase0. /** * Format `contribution` into an efficient `aggregate` to add more contributions in with aggregateContributionInto() */ -function attestationToAggregate(attestation: phase0.Attestation): AggregateFast { +function attestationToAggregate(attestation: allForks.Attestation): AggregateFast { + if ("committeeBits" in attestation) { + return { + data: attestation.data, + // clone because it will be mutated + aggregationBits: attestation.aggregationBits.clone(), + committeeBits: attestation.committeeBits, + signature: signatureFromBytesNoCheck(attestation.signature), + }; + } return { data: attestation.data, // clone because it will be mutated @@ -213,10 +246,19 @@ function attestationToAggregate(attestation: phase0.Attestation): AggregateFast /** * Unwrap AggregateFast to phase0.Attestation */ -function fastToAttestation(aggFast: AggregateFast): phase0.Attestation { - return { - data: aggFast.data, - aggregationBits: aggFast.aggregationBits, - signature: aggFast.signature.toBytes(PointFormat.compressed), - }; +function fastToAttestation(aggFast: AggregateFast): allForks.Attestation { + if ("committeeBits" in aggFast) { + return { + data: aggFast.data, + aggregationBits: aggFast.aggregationBits, + committeeBits: aggFast.committeeBits, + signature: aggFast.signature.toBytes(PointFormat.compressed), + }; + } else { + return { + data: aggFast.data, + aggregationBits: aggFast.aggregationBits, + signature: aggFast.signature.toBytes(PointFormat.compressed), + }; + } } diff --git a/packages/beacon-node/src/chain/opPools/opPool.ts b/packages/beacon-node/src/chain/opPools/opPool.ts index 1fdee886ff1d..23c29d7efab6 100644 --- a/packages/beacon-node/src/chain/opPools/opPool.ts +++ b/packages/beacon-node/src/chain/opPools/opPool.ts @@ -14,6 +14,7 @@ import { BLS_WITHDRAWAL_PREFIX, MAX_ATTESTER_SLASHINGS, ForkSeq, + MAX_ATTESTER_SLASHINGS_ELECTRA, } from "@lodestar/params"; import {Epoch, phase0, capella, ssz, ValidatorIndex, allForks} from "@lodestar/types"; import {IBeaconDb} from "../../db/index.js"; @@ -173,7 +174,7 @@ export class OpPool { blockType: BlockType, metrics: Metrics | null ): [ - phase0.AttesterSlashing[], + allForks.AttesterSlashing[], phase0.ProposerSlashing[], phase0.SignedVoluntaryExit[], capella.SignedBLSToExecutionChange[], @@ -207,7 +208,8 @@ export class OpPool { }); const endAttesterSlashings = stepsMetrics?.startTimer(); - const attesterSlashings: phase0.AttesterSlashing[] = []; + const attesterSlashings: allForks.AttesterSlashing[] = []; + const maxAttesterSlashing = stateFork >= ForkSeq.electra ? MAX_ATTESTER_SLASHINGS_ELECTRA : MAX_ATTESTER_SLASHINGS; attesterSlashing: for (const attesterSlashing of this.attesterSlashings.values()) { /** Indices slashable in this attester slashing */ const slashableIndices = new Set(); @@ -222,7 +224,7 @@ export class OpPool { if (isSlashableAtEpoch(validator, stateEpoch)) { slashableIndices.add(index); } - if (attesterSlashings.length >= MAX_ATTESTER_SLASHINGS) { + if (attesterSlashings.length >= maxAttesterSlashing) { break attesterSlashing; } } diff --git a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts index 430464683493..7950466570ce 100644 --- a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts @@ -1,6 +1,6 @@ import {toHexString} from "@chainsafe/ssz"; -import {ForkName} from "@lodestar/params"; -import {phase0, RootHex, ssz} from "@lodestar/types"; +import {ForkName, ForkSeq} from "@lodestar/params"; +import {allForks, electra, phase0, RootHex, ssz} from "@lodestar/types"; import { computeEpochAtSlot, isAggregatorFromCommitteeLength, @@ -20,7 +20,7 @@ import { } from "./attestation.js"; export type AggregateAndProofValidationResult = { - indexedAttestation: phase0.IndexedAttestation; + indexedAttestation: allForks.IndexedAttestation; committeeIndices: Uint32Array; attDataRootHex: RootHex; }; @@ -41,7 +41,7 @@ export async function validateApiAggregateAndProof( export async function validateGossipAggregateAndProof( fork: ForkName, chain: IBeaconChain, - signedAggregateAndProof: phase0.SignedAggregateAndProof, + signedAggregateAndProof: allForks.SignedAggregateAndProof, serializedData: Uint8Array ): Promise { return validateAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData); @@ -50,7 +50,7 @@ export async function validateGossipAggregateAndProof( async function validateAggregateAndProof( fork: ForkName, chain: IBeaconChain, - signedAggregateAndProof: phase0.SignedAggregateAndProof, + signedAggregateAndProof: allForks.SignedAggregateAndProof, serializedData: Uint8Array | null = null, opts: {skipValidationKnownAttesters: boolean; prioritizeBls: boolean} = { skipValidationKnownAttesters: false, @@ -74,7 +74,21 @@ async function validateAggregateAndProof( const attDataBase64 = serializedData ? getAttDataBase64FromSignedAggregateAndProofSerialized(serializedData) : null; const cachedAttData = attDataBase64 ? chain.seenAttestationDatas.get(attSlot, attDataBase64) : null; - const attIndex = attData.index; + let attIndex; + if (ForkSeq[fork] >= ForkSeq.electra) { + attIndex = (aggregate as electra.Attestation).committeeBits.getSingleTrueBit(); + // [REJECT] len(committee_indices) == 1, where committee_indices = get_committee_indices(aggregate) + if (attIndex === null) { + throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET}); + } + // [REJECT] aggregate.data.index == 0 + if (attData.index === 0) { + throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}); + } + } else { + attIndex = attData.index; + } + const attEpoch = computeEpochAtSlot(attSlot); const attTarget = attData.target; const targetEpoch = attTarget.epoch; @@ -163,11 +177,16 @@ async function validateAggregateAndProof( throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS}); } const attestingIndices = aggregate.aggregationBits.intersectValues(committeeIndices); - const indexedAttestation: phase0.IndexedAttestation = { + + const indexedAttestationContent = { attestingIndices, data: attData, signature: aggregate.signature, }; + const indexedAttestation = + ForkSeq[fork] >= ForkSeq.electra + ? (indexedAttestationContent as electra.IndexedAttestation) + : (indexedAttestationContent as phase0.IndexedAttestation); // TODO: Check this before regen // [REJECT] The attestation has participants -- that is, diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index 7b80d75e3063..a186b1325396 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -1,5 +1,5 @@ import {toHexString} from "@chainsafe/ssz"; -import {phase0, Epoch, Root, Slot, RootHex, ssz} from "@lodestar/types"; +import {phase0, Epoch, Root, Slot, RootHex, ssz, allForks, electra} from "@lodestar/types"; import {ProtoBlock} from "@lodestar/fork-choice"; import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, ForkName, ForkSeq, DOMAIN_BEACON_ATTESTER} from "@lodestar/params"; import { @@ -20,6 +20,7 @@ import { AttDataBase64, getAggregationBitsFromAttestationSerialized, getAttDataBase64FromAttestationSerialized, + getCommitteeBitsFromAttestationSerialized, getSignatureFromAttestationSerialized, } from "../../util/sszBytes.js"; import {AttestationDataCacheEntry} from "../seenCache/seenAttestationData.js"; @@ -34,8 +35,8 @@ export type BatchResult = { }; export type AttestationValidationResult = { - attestation: phase0.Attestation; - indexedAttestation: phase0.IndexedAttestation; + attestation: allForks.Attestation; + indexedAttestation: allForks.IndexedAttestation; subnet: number; attDataRootHex: RootHex; }; @@ -43,7 +44,7 @@ export type AttestationValidationResult = { export type AttestationOrBytes = ApiAttestation | GossipAttestation; /** attestation from api */ -export type ApiAttestation = {attestation: phase0.Attestation; serializedData: null}; +export type ApiAttestation = {attestation: phase0.Attestation; serializedData: null}; // TODO Electra: add new attestation type /** attestation from gossip */ export type GossipAttestation = { @@ -248,7 +249,7 @@ async function validateGossipAttestationNoSignatureCheck( // Run the checks that happen before an indexed attestation is constructed. let attestationOrCache: - | {attestation: phase0.Attestation; cache: null} + | {attestation: allForks.Attestation; cache: null} | {attestation: null; cache: AttestationDataCacheEntry; serializedData: Uint8Array}; let attDataBase64: AttDataBase64 | null = null; if (attestationOrBytes.serializedData) { @@ -260,7 +261,7 @@ async function validateGossipAttestationNoSignatureCheck( attestationOrBytes.attDataBase64 ?? getAttDataBase64FromAttestationSerialized(attestationOrBytes.serializedData); const cachedAttData = attDataBase64 !== null ? chain.seenAttestationDatas.get(attSlot, attDataBase64) : null; if (cachedAttData === null) { - const attestation = sszDeserializeAttestation(attestationOrBytes.serializedData); + const attestation = sszDeserializeAttestation(fork, attestationOrBytes.serializedData); // only deserialize on the first AttestationData that's not cached attestationOrCache = {attestation, cache: null}; } else { @@ -276,11 +277,34 @@ async function validateGossipAttestationNoSignatureCheck( ? attestationOrCache.attestation.data : attestationOrCache.cache.attestationData; const attSlot = attData.slot; - const attIndex = attData.index; const attEpoch = computeEpochAtSlot(attSlot); const attTarget = attData.target; const targetEpoch = attTarget.epoch; + let attIndex; + if (ForkSeq[fork] >= ForkSeq.electra) { + const committeeBits = attestationOrCache.attestation + ? (attestationOrCache.attestation as electra.Attestation).committeeBits + : getCommitteeBitsFromAttestationSerialized(attestationOrCache.serializedData); + + if (committeeBits === null) { + throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.INVALID_SERIALIZED_BYTES}); + } + + attIndex = committeeBits.getSingleTrueBit(); + // [REJECT] len(committee_indices) == 1, where committee_indices = get_committee_indices(aggregate) + if (attIndex === null) { + throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET}); + } + + // [REJECT] aggregate.data.index == 0 + if (attData.index === 0) { + throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}); + } + } else { + attIndex = attData.index; + } + chain.metrics?.gossipAttestation.attestationSlotToClockSlot.observe( {caller: RegenCaller.validateGossipAttestation}, chain.clock.currentSlot - attSlot @@ -452,13 +476,17 @@ async function validateGossipAttestationNoSignatureCheck( } // no signature check, leave that for step1 - const indexedAttestation: phase0.IndexedAttestation = { + const indexedAttestationContent = { attestingIndices, data: attData, signature, }; + const indexedAttestation = + ForkSeq[fork] >= ForkSeq.electra + ? (indexedAttestationContent as electra.IndexedAttestation) + : (indexedAttestationContent as phase0.IndexedAttestation); - const attestation: phase0.Attestation = attestationOrCache.attestation + const attestation: allForks.Attestation = attestationOrCache.attestation ? attestationOrCache.attestation : { aggregationBits, @@ -698,6 +726,10 @@ function verifyAttestationTargetRoot(headBlock: ProtoBlock, targetRoot: Root, at } } +/** + * Get a list of indices of validators in the given committee + * attestationIndex - Index of the committee in shuffling.committees + */ export function getCommitteeIndices( shuffling: EpochShuffling, attestationSlot: Slot, diff --git a/packages/beacon-node/src/chain/validation/attesterSlashing.ts b/packages/beacon-node/src/chain/validation/attesterSlashing.ts index 818812526fb3..11a499c9bb53 100644 --- a/packages/beacon-node/src/chain/validation/attesterSlashing.ts +++ b/packages/beacon-node/src/chain/validation/attesterSlashing.ts @@ -9,7 +9,7 @@ import {AttesterSlashingError, AttesterSlashingErrorCode, GossipAction} from ".. export async function validateApiAttesterSlashing( chain: IBeaconChain, - attesterSlashing: phase0.AttesterSlashing + attesterSlashing: phase0.AttesterSlashing // TODO Electra: Handle electra.AttesterSlashing ): Promise { const prioritizeBls = true; return validateAttesterSlashing(chain, attesterSlashing, prioritizeBls); diff --git a/packages/beacon-node/src/metrics/validatorMonitor.ts b/packages/beacon-node/src/metrics/validatorMonitor.ts index 1104b1198fae..11fe3b17a357 100644 --- a/packages/beacon-node/src/metrics/validatorMonitor.ts +++ b/packages/beacon-node/src/metrics/validatorMonitor.ts @@ -14,7 +14,7 @@ import {RootHex, allForks, altair, deneb} from "@lodestar/types"; import {ChainConfig, ChainForkConfig} from "@lodestar/config"; import {ForkSeq, INTERVALS_PER_SLOT, MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH} from "@lodestar/params"; import {Epoch, Slot, ValidatorIndex} from "@lodestar/types"; -import {IndexedAttestation, SignedAggregateAndProof} from "@lodestar/types/phase0"; +import {IndexedAttestation, SignedAggregateAndProof} from "@lodestar/types/allForks"; import {GENESIS_SLOT} from "../constants/constants.js"; import {LodestarMetrics} from "./metrics/lodestar.js"; diff --git a/packages/beacon-node/src/network/gossip/interface.ts b/packages/beacon-node/src/network/gossip/interface.ts index df26c2328c70..eb08c4ffe937 100644 --- a/packages/beacon-node/src/network/gossip/interface.ts +++ b/packages/beacon-node/src/network/gossip/interface.ts @@ -71,8 +71,8 @@ export type SSZTypeOfGossipTopic = T extends {type: infer export type GossipTypeMap = { [GossipType.beacon_block]: allForks.SignedBeaconBlock; [GossipType.blob_sidecar]: deneb.BlobSidecar; - [GossipType.beacon_aggregate_and_proof]: phase0.SignedAggregateAndProof; - [GossipType.beacon_attestation]: phase0.Attestation; + [GossipType.beacon_aggregate_and_proof]: allForks.SignedAggregateAndProof; + [GossipType.beacon_attestation]: allForks.Attestation; [GossipType.voluntary_exit]: phase0.SignedVoluntaryExit; [GossipType.proposer_slashing]: phase0.ProposerSlashing; [GossipType.attester_slashing]: phase0.AttesterSlashing; @@ -86,8 +86,10 @@ export type GossipTypeMap = { export type GossipFnByType = { [GossipType.beacon_block]: (signedBlock: allForks.SignedBeaconBlock) => Promise | void; [GossipType.blob_sidecar]: (blobSidecar: deneb.BlobSidecar) => Promise | void; - [GossipType.beacon_aggregate_and_proof]: (aggregateAndProof: phase0.SignedAggregateAndProof) => Promise | void; - [GossipType.beacon_attestation]: (attestation: phase0.Attestation) => Promise | void; + [GossipType.beacon_aggregate_and_proof]: ( + aggregateAndProof: allForks.SignedAggregateAndProof + ) => Promise | void; + [GossipType.beacon_attestation]: (attestation: allForks.Attestation) => Promise | void; [GossipType.voluntary_exit]: (voluntaryExit: phase0.SignedVoluntaryExit) => Promise | void; [GossipType.proposer_slashing]: (proposerSlashing: phase0.ProposerSlashing) => Promise | void; [GossipType.attester_slashing]: (attesterSlashing: phase0.AttesterSlashing) => Promise | void; diff --git a/packages/beacon-node/src/network/gossip/topic.ts b/packages/beacon-node/src/network/gossip/topic.ts index c5cd68ffa1de..cf7238410718 100644 --- a/packages/beacon-node/src/network/gossip/topic.ts +++ b/packages/beacon-node/src/network/gossip/topic.ts @@ -1,4 +1,4 @@ -import {phase0, ssz} from "@lodestar/types"; +import {allForks, ssz} from "@lodestar/types"; import {ForkDigestContext} from "@lodestar/config"; import { ATTESTATION_SUBNET_COUNT, @@ -87,9 +87,9 @@ export function getGossipSSZType(topic: GossipTopic) { case GossipType.blob_sidecar: return ssz.deneb.BlobSidecar; case GossipType.beacon_aggregate_and_proof: - return ssz.phase0.SignedAggregateAndProof; + return ssz.allForks[topic.fork].SignedAggregateAndProof; case GossipType.beacon_attestation: - return ssz.phase0.Attestation; + return ssz.allForks[topic.fork].Attestation; case GossipType.proposer_slashing: return ssz.phase0.ProposerSlashing; case GossipType.attester_slashing: @@ -128,9 +128,10 @@ export function sszDeserialize(topic: T, serializedData: /** * Deserialize a gossip serialized data into an Attestation object. */ -export function sszDeserializeAttestation(serializedData: Uint8Array): phase0.Attestation { +export function sszDeserializeAttestation(fork: ForkName, serializedData: Uint8Array): allForks.Attestation { + const sszType = ssz.allForks[fork].Attestation; try { - return ssz.phase0.Attestation.deserialize(serializedData); + return sszType.deserialize(serializedData); } catch (e) { throw new GossipActionError(GossipAction.REJECT, {code: GossipErrorCode.INVALID_SERIALIZED_BYTES_ERROR_CODE}); } diff --git a/packages/beacon-node/src/network/interface.ts b/packages/beacon-node/src/network/interface.ts index aeeb61f1feb2..083a513586a2 100644 --- a/packages/beacon-node/src/network/interface.ts +++ b/packages/beacon-node/src/network/interface.ts @@ -61,7 +61,7 @@ export interface INetwork extends INetworkCorePublic { // Gossip publishBeaconBlock(signedBlock: allForks.SignedBeaconBlock): Promise; publishBlobSidecar(blobSidecar: deneb.BlobSidecar): Promise; - publishBeaconAggregateAndProof(aggregateAndProof: phase0.SignedAggregateAndProof): Promise; + publishBeaconAggregateAndProof(aggregateAndProof: allForks.SignedAggregateAndProof): Promise; publishBeaconAttestation(attestation: phase0.Attestation, subnet: number): Promise; publishVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise; publishBlsToExecutionChange(blsToExecutionChange: capella.SignedBLSToExecutionChange): Promise; diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index be8bb5114d40..84cbabfc7e14 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -304,7 +304,7 @@ export class Network implements INetwork { }); } - async publishBeaconAggregateAndProof(aggregateAndProof: phase0.SignedAggregateAndProof): Promise { + async publishBeaconAggregateAndProof(aggregateAndProof: allForks.SignedAggregateAndProof): Promise { const fork = this.config.getForkName(aggregateAndProof.message.aggregate.data.slot); return this.publishGossip( {type: GossipType.beacon_aggregate_and_proof, fork}, diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 1a71cc7de334..55de125fed29 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -422,7 +422,11 @@ function getDefaultHandlers(modules: ValidatorFnsModules, options: GossipHandler validationResult = await validateGossipAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData); } catch (e) { if (e instanceof AttestationError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue(ssz.phase0.SignedAggregateAndProof, signedAggregateAndProof, "gossip_reject"); + chain.persistInvalidSszValue( + ssz.allForks[fork].SignedAggregateAndProof, + signedAggregateAndProof, + "gossip_reject" + ); } throw e; } @@ -451,7 +455,10 @@ function getDefaultHandlers(modules: ValidatorFnsModules, options: GossipHandler } } - chain.emitter.emit(routes.events.EventType.attestation, signedAggregateAndProof.message.aggregate); + chain.emitter.emit(routes.events.EventType.attestation, { + version: fork, + data: signedAggregateAndProof.message.aggregate, + }); }, [GossipType.beacon_attestation]: async ({ gossipData, @@ -503,7 +510,7 @@ function getDefaultHandlers(modules: ValidatorFnsModules, options: GossipHandler } } - chain.emitter.emit(routes.events.EventType.attestation, attestation); + chain.emitter.emit(routes.events.EventType.attestation, {version: fork, data: attestation}); }, [GossipType.attester_slashing]: async ({ @@ -523,7 +530,7 @@ function getDefaultHandlers(modules: ValidatorFnsModules, options: GossipHandler logger.error("Error adding attesterSlashing to pool", {}, e as Error); } - chain.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing); + chain.emitter.emit(routes.events.EventType.attesterSlashing, {version: topic.fork, data: attesterSlashing}); }, [GossipType.proposer_slashing]: async ({ @@ -711,7 +718,7 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp } } - chain.emitter.emit(routes.events.EventType.attestation, attestation); + chain.emitter.emit(routes.events.EventType.attestation, {version: fork, data: attestation}); } if (batchableBls) { diff --git a/packages/beacon-node/test/unit/api/impl/events/events.test.ts b/packages/beacon-node/test/unit/api/impl/events/events.test.ts index e031c3ac9958..b1f85b5e6e44 100644 --- a/packages/beacon-node/test/unit/api/impl/events/events.test.ts +++ b/packages/beacon-node/test/unit/api/impl/events/events.test.ts @@ -2,6 +2,7 @@ import {describe, it, expect, beforeEach, afterEach, vi, MockedObject} from "vit import {routes} from "@lodestar/api"; import {config} from "@lodestar/config/default"; import {ssz} from "@lodestar/types"; +import {ForkName} from "@lodestar/params"; import {BeaconChain, ChainEventEmitter, HeadEventData} from "../../../../../src/chain/index.js"; import {getEventsApi} from "../../../../../src/api/impl/events/index.js"; import {ZERO_HASH_HEX} from "../../../../../src/constants/constants.js"; @@ -66,7 +67,10 @@ describe("Events api impl", function () { it("should ignore not sent topics", async function () { const events = getEvents([routes.events.EventType.head]); - chainEventEmmitter.emit(routes.events.EventType.attestation, ssz.phase0.Attestation.defaultValue()); + chainEventEmmitter.emit(routes.events.EventType.attestation, { + version: ForkName.phase0, + data: ssz.phase0.Attestation.defaultValue(), + }); chainEventEmmitter.emit(routes.events.EventType.head, headEventData); expect(events).toHaveLength(1); 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 3c248ad4d194..c375c9956758 100644 --- a/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/unit/chain/opPools/aggregatedAttestationPool.test.ts @@ -3,13 +3,21 @@ import bls from "@chainsafe/bls"; import {BitArray, fromHexString, toHexString} from "@chainsafe/ssz"; import {describe, it, expect, beforeEach, beforeAll, afterEach, vi} from "vitest"; import {CachedBeaconStateAllForks, newFilledArray} from "@lodestar/state-transition"; -import {FAR_FUTURE_EPOCH, ForkName, MAX_EFFECTIVE_BALANCE, SLOTS_PER_EPOCH} from "@lodestar/params"; +import { + FAR_FUTURE_EPOCH, + ForkName, + MAX_COMMITTEES_PER_SLOT, + MAX_EFFECTIVE_BALANCE, + SLOTS_PER_EPOCH, +} from "@lodestar/params"; import {ssz, phase0} from "@lodestar/types"; import {CachedBeaconStateAltair} from "@lodestar/state-transition/src/types.js"; import {MockedForkChoice, getMockedForkChoice} from "../../../mocks/mockedBeaconChain.js"; import { + aggregateConsolidation, AggregatedAttestationPool, aggregateInto, + AttestationsConsolidation, getNotSeenValidatorsFn, MatchingDataAttestationGroup, } from "../../../../src/chain/opPools/aggregatedAttestationPool.js"; @@ -81,11 +89,11 @@ describe("AggregatedAttestationPool", function () { vi.clearAllMocks(); }); - it("getParticipationFn", () => { + it("getNotSeenValidatorsFn", () => { // previousEpochParticipation and currentEpochParticipation is created inside generateCachedState // 0 and 1 are fully participated const notSeenValidatorFn = getNotSeenValidatorsFn(altairState); - const participation = notSeenValidatorFn(currentEpoch, committee); + const participation = notSeenValidatorFn(currentEpoch, currentSlot, committeeIndex); // seen attesting indices are 0, 1 => not seen are 2, 3 expect(participation).toEqual( // { @@ -280,6 +288,7 @@ describe("MatchingDataAttestationGroup.getAttestationsForBlock", () => { } } const attestationsForBlock = attestationGroup.getAttestationsForBlock( + ForkName.phase0, // notSeenValidatorIndices, notSeenAttestingIndices ); @@ -319,3 +328,75 @@ describe("MatchingDataAttestationGroup aggregateInto", function () { expect(aggregatedSignature.verifyAggregate([sk1.toPublicKey(), sk2.toPublicKey()], attestationDataRoot)).toBe(true); }); }); + +describe("aggregateConsolidation", function () { + const sk0 = bls.SecretKey.fromBytes(Buffer.alloc(32, 1)); + const sk1 = bls.SecretKey.fromBytes(Buffer.alloc(32, 2)); + const sk2 = bls.SecretKey.fromBytes(Buffer.alloc(32, 3)); + const skArr = [sk0, sk1, sk2]; + const testCases: { + name: string; + committeeIndices: number[]; + aggregationBitsArr: Array[]; + expectedAggregationBits: Array; + expectedCommitteeBits: Array; + }[] = [ + // note that bit index starts from the right + { + name: "test case 0", + committeeIndices: [0, 1, 2], + aggregationBitsArr: [[0b111], [0b011], [0b111]], + expectedAggregationBits: [0b11011111, 0b1], + expectedCommitteeBits: [true, true, true, false], + }, + { + name: "test case 1", + committeeIndices: [2, 3, 1], + aggregationBitsArr: [[0b100], [0b010], [0b001]], + expectedAggregationBits: [0b10100001, 0b0], + expectedCommitteeBits: [false, true, true, true], + }, + ]; + for (const { + name, + committeeIndices, + aggregationBitsArr, + expectedAggregationBits, + expectedCommitteeBits, + } of testCases) { + it(name, () => { + const attData = ssz.phase0.AttestationData.defaultValue(); + const consolidation: AttestationsConsolidation = { + byCommittee: new Map(), + attData: attData, + totalNotSeenCount: 0, + score: 0, + }; + // to simplify, instead of signing the signingRoot, just sign the attData root + const sigArr = skArr.map((sk) => sk.sign(ssz.phase0.AttestationData.hashTreeRoot(attData))); + const attestationSeed = ssz.electra.Attestation.defaultValue(); + for (let i = 0; i < committeeIndices.length; i++) { + const committeeIndex = committeeIndices[i]; + const commiteeBits = BitArray.fromBoolArray( + Array.from({length: MAX_COMMITTEES_PER_SLOT}, (_, i) => i === committeeIndex) + ); + const aggAttestation = { + ...attestationSeed, + aggregationBits: new BitArray(new Uint8Array(aggregationBitsArr[i]), 3), + committeeBits: commiteeBits, + signature: sigArr[i].toBytes(), + }; + consolidation.byCommittee.set(committeeIndex, { + attestation: aggAttestation, + notSeenAttesterCount: aggregationBitsArr[i].filter((item) => item).length, + }); + } + + const finalAttestation = aggregateConsolidation(consolidation); + expect(finalAttestation.aggregationBits.uint8Array).toEqual(new Uint8Array(expectedAggregationBits)); + expect(finalAttestation.committeeBits.toBoolArray()).toEqual(expectedCommitteeBits); + expect(finalAttestation.data).toEqual(attData); + expect(finalAttestation.signature).toEqual(bls.Signature.aggregate(sigArr).toBytes()); + }); + } +}); diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index aa5b86f0e64e..e481ba20552f 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -156,7 +156,7 @@ export interface IForkChoice { * The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it * will not be run here. */ - onAttestation(attestation: phase0.IndexedAttestation, attDataRoot: string, forceImport?: boolean): void; + onAttestation(attestation: allForks.IndexedAttestation, attDataRoot: string, forceImport?: boolean): void; /** * Register attester slashing in order not to consider their votes in `getHead` * diff --git a/packages/state-transition/src/signatureSets/attesterSlashings.ts b/packages/state-transition/src/signatureSets/attesterSlashings.ts index 6a985f84f64e..9b04fceac45f 100644 --- a/packages/state-transition/src/signatureSets/attesterSlashings.ts +++ b/packages/state-transition/src/signatureSets/attesterSlashings.ts @@ -1,4 +1,4 @@ -import {allForks, phase0, ssz} from "@lodestar/types"; +import {allForks, ssz} from "@lodestar/types"; import {DOMAIN_BEACON_ATTESTER} from "@lodestar/params"; import {computeSigningRoot, computeStartSlotAtEpoch, ISignatureSet, SignatureSetType} from "../util/index.js"; import {CachedBeaconStateAllForks} from "../types.js"; @@ -16,7 +16,7 @@ export function getAttesterSlashingsSignatureSets( /** Get signature sets from a single AttesterSlashing object */ export function getAttesterSlashingSignatureSets( state: CachedBeaconStateAllForks, - attesterSlashing: phase0.AttesterSlashing + attesterSlashing: allForks.AttesterSlashing ): ISignatureSet[] { return [attesterSlashing.attestation1, attesterSlashing.attestation2].map((attestation) => getIndexedAttestationBigintSignatureSet(state, attestation) @@ -25,7 +25,7 @@ export function getAttesterSlashingSignatureSets( export function getIndexedAttestationBigintSignatureSet( state: CachedBeaconStateAllForks, - indexedAttestation: phase0.IndexedAttestationBigint + indexedAttestation: allForks.IndexedAttestationBigint ): ISignatureSet { const slot = computeStartSlotAtEpoch(Number(indexedAttestation.data.target.epoch as bigint)); const domain = state.config.getDomain(state.slot, DOMAIN_BEACON_ATTESTER, slot); diff --git a/packages/state-transition/src/signatureSets/index.ts b/packages/state-transition/src/signatureSets/index.ts index a6bdc7f478bd..0e149f508936 100644 --- a/packages/state-transition/src/signatureSets/index.ts +++ b/packages/state-transition/src/signatureSets/index.ts @@ -33,6 +33,9 @@ export function getBlockSignatureSets( skipProposerSignature?: boolean; } ): ISignatureSet[] { + // fork based validations + const fork = state.config.getForkSeq(signedBlock.message.slot); + const signatureSets = [ getRandaoRevealSignatureSet(state, signedBlock.message), ...getProposerSlashingsSignatureSets(state, signedBlock), @@ -45,9 +48,6 @@ export function getBlockSignatureSets( signatureSets.push(getBlockProposerSignatureSet(state, signedBlock)); } - // fork based validations - const fork = state.config.getForkSeq(signedBlock.message.slot); - // Only after altair fork, validate tSyncCommitteeSignature if (fork >= ForkSeq.altair) { const syncCommitteeSignatureSet = getSyncCommitteeSignatureSet( diff --git a/packages/types/src/allForks/types.ts b/packages/types/src/allForks/types.ts index f116bde20b70..8fda9704eb98 100644 --- a/packages/types/src/allForks/types.ts +++ b/packages/types/src/allForks/types.ts @@ -162,6 +162,7 @@ export type AllForksTypes = { BeaconBlock: BeaconBlock; SignedBeaconBlock: SignedBeaconBlock; BeaconState: BeaconState; + Attestation: Attestation; Metadata: Metadata; ExecutionPayload: ExecutionPayload; ExecutionPayloadHeader: ExecutionPayloadHeader; @@ -263,6 +264,11 @@ export type AllForksSSZTypes = { | typeof electraSsz.BeaconState >; Metadata: AllForksTypeOf; + SignedAggregateAndProof: AllForksTypeOf< + typeof phase0Ssz.SignedAggregateAndProof | typeof electraSsz.SignedAggregateAndProof + >; + Attestation: AllForksTypeOf; + AttesterSlashing: AllForksTypeOf; }; export type AllForksExecutionSSZTypes = { diff --git a/packages/validator/src/services/attestation.ts b/packages/validator/src/services/attestation.ts index 57a8a7621a97..39c3cfac641d 100644 --- a/packages/validator/src/services/attestation.ts +++ b/packages/validator/src/services/attestation.ts @@ -1,8 +1,9 @@ import {toHexString} from "@chainsafe/ssz"; -import {BLSSignature, phase0, Slot, ssz} from "@lodestar/types"; +import {allForks, BLSSignature, phase0, Slot, ssz} from "@lodestar/types"; import {computeEpochAtSlot, isAggregatorFromCommitteeLength} from "@lodestar/state-transition"; import {sleep} from "@lodestar/utils"; import {ApiClient, routes} from "@lodestar/api"; +import {ChainForkConfig} from "@lodestar/config"; import {IClock, LoggerVc} from "../util/index.js"; import {PubkeyHex} from "../types.js"; import {Metrics} from "../metrics.js"; @@ -41,6 +42,7 @@ export class AttestationService { private readonly emitter: ValidatorEventEmitter, chainHeadTracker: ChainHeaderTracker, private readonly metrics: Metrics | null, + private readonly config: ChainForkConfig, private readonly opts?: AttestationServiceOpts ) { this.dutiesService = new AttestationDutiesService(logger, api, clock, validatorStore, chainHeadTracker, metrics, { @@ -260,7 +262,7 @@ export class AttestationService { const aggregate = res.value(); this.metrics?.numParticipantsInAggregate.observe(aggregate.aggregationBits.getTrueBitIndexes().length); - const signedAggregateAndProofs: phase0.SignedAggregateAndProof[] = []; + const signedAggregateAndProofs: allForks.SignedAggregateAndProof[] = []; await Promise.all( duties.map(async ({duty, selectionProof}) => { diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 6cd9ed8dc065..1d5d73377946 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -19,6 +19,7 @@ import { DOMAIN_SYNC_COMMITTEE, DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, DOMAIN_APPLICATION_BUILDER, + ForkSeq, } from "@lodestar/params"; import { allForks, @@ -26,6 +27,7 @@ import { bellatrix, BLSPubkey, BLSSignature, + electra, Epoch, phase0, Root, @@ -490,7 +492,7 @@ export class ValidatorStore { duty: routes.validator.AttesterDuty, attestationData: phase0.AttestationData, currentEpoch: Epoch - ): Promise { + ): Promise { // Make sure the target epoch is not higher than the current epoch to avoid potential attacks. if (attestationData.target.epoch > currentEpoch) { throw Error( @@ -522,21 +524,30 @@ export class ValidatorStore { data: attestationData, }; - return { - aggregationBits: BitArray.fromSingleBit(duty.committeeLength, duty.validatorCommitteeIndex), - data: attestationData, - signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage), - }; + if (this.config.getForkSeq(duty.slot) >= ForkSeq.electra) { + return { + aggregationBits: BitArray.fromSingleBit(duty.committeeLength, duty.validatorCommitteeIndex), + data: attestationData, + committeeBits: BitArray.fromSingleBit(duty.committeesAtSlot, duty.committeeIndex), + signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage), + } as electra.Attestation; + } else { + return { + aggregationBits: BitArray.fromSingleBit(duty.committeeLength, duty.validatorCommitteeIndex), + data: attestationData, + signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage), + } as phase0.Attestation; + } } async signAggregateAndProof( duty: routes.validator.AttesterDuty, selectionProof: BLSSignature, - aggregate: phase0.Attestation - ): Promise { + aggregate: allForks.Attestation + ): Promise { this.validateAttestationDuty(duty, aggregate.data); - const aggregateAndProof: phase0.AggregateAndProof = { + const aggregateAndProof: allForks.AggregateAndProof = { aggregate, aggregatorIndex: duty.validatorIndex, selectionProof, @@ -544,7 +555,10 @@ export class ValidatorStore { const signingSlot = aggregate.data.slot; const domain = this.config.getDomain(signingSlot, DOMAIN_AGGREGATE_AND_PROOF); - const signingRoot = computeSigningRoot(ssz.phase0.AggregateAndProof, aggregateAndProof, domain); + const signingRoot = + this.config.getForkSeq(duty.slot) >= ForkSeq.electra + ? computeSigningRoot(ssz.electra.AggregateAndProof, aggregateAndProof, domain) + : computeSigningRoot(ssz.phase0.AggregateAndProof, aggregateAndProof, domain); const signableMessage: SignableMessage = { type: SignableMessageType.AGGREGATE_AND_PROOF, @@ -785,6 +799,9 @@ export class ValidatorStore { `Inconsistent duties during signing: duty.committeeIndex ${duty.committeeIndex} != att.committeeIndex ${data.index}` ); } + if (this.config.getForkSeq(duty.slot) >= ForkSeq.electra && data.index !== 0) { + throw Error(`Attestataion data index must be 0 post electra: index ${data.index}`); + } } private assertDoppelgangerSafe(pubKey: PubkeyHex | BLSPubkey): void { diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts index 9cb9f2e2d840..86c6084ffe4e 100644 --- a/packages/validator/src/validator.ts +++ b/packages/validator/src/validator.ts @@ -228,6 +228,7 @@ export class Validator { emitter, chainHeaderTracker, metrics, + config, { afterBlockDelaySlotFraction: opts.afterBlockDelaySlotFraction, disableAttestationGrouping: opts.disableAttestationGrouping || opts.distributed, diff --git a/packages/validator/test/unit/services/attestation.test.ts b/packages/validator/test/unit/services/attestation.test.ts index e1254d1c6a52..9227af9c8e08 100644 --- a/packages/validator/test/unit/services/attestation.test.ts +++ b/packages/validator/test/unit/services/attestation.test.ts @@ -3,6 +3,8 @@ import bls from "@chainsafe/bls"; import {toHexString} from "@chainsafe/ssz"; import {ssz} from "@lodestar/types"; import {routes} from "@lodestar/api"; +import {createChainForkConfig} from "@lodestar/config"; +import {config} from "@lodestar/config/default"; import {AttestationService, AttestationServiceOpts} from "../../../src/services/attestation.js"; import {AttDutyAndProof} from "../../../src/services/attestationDuties.js"; import {ValidatorStore} from "../../../src/services/validatorStore.js"; @@ -63,6 +65,7 @@ describe("AttestationService", function () { emitter, chainHeadTracker, null, + createChainForkConfig(config), opts ); From 17058880665ca281ba170165d6728a7a19750a61 Mon Sep 17 00:00:00 2001 From: twoeths Date: Wed, 8 May 2024 18:09:39 +0700 Subject: [PATCH 13/51] fix: attestation pool for electra (#6744) * feat: attestationPool to group by slot by data root by committee index for electra * fix: gossip validation and assert.notNull() util * fix: remove light-client stats.html * fix: lint and check-types --- packages/api/src/beacon/routes/validator.ts | 16 +-- .../api/test/unit/beacon/testData/events.ts | 98 ++++++++++--------- .../test/unit/beacon/testData/validator.ts | 4 +- .../src/api/impl/beacon/pool/index.ts | 3 +- .../src/api/impl/validator/index.ts | 11 ++- .../opPools/aggregatedAttestationPool.ts | 8 +- .../src/chain/opPools/attestationPool.ts | 79 ++++++--------- .../src/chain/validation/aggregateAndProof.ts | 2 +- .../src/chain/validation/attestation.ts | 4 +- .../test/perf/epoch/epochAltair.test.ts | 4 +- .../test/perf/epoch/epochCapella.test.ts | 4 +- .../test/perf/epoch/epochPhase0.test.ts | 4 +- .../processEffectiveBalanceUpdates.test.ts | 3 +- .../perf/epoch/processRegistryUpdates.test.ts | 3 +- packages/utils/src/assert.ts | 12 +++ packages/utils/test/unit/assert.test.ts | 12 ++- .../validator/src/services/attestation.ts | 8 +- .../validator/src/services/validatorStore.ts | 3 +- 18 files changed, 149 insertions(+), 129 deletions(-) diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index 25ab6851e150..56c23ceb4866 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -427,9 +427,9 @@ export type Endpoints = { /** HashTreeRoot of AttestationData that validator want's aggregated */ attestationDataRoot: Root; slot: Slot; - index: number; + committeeIndex: number; }, - {query: {attestation_data_root: string; slot: number; index: number}}, + {query: {attestation_data_root: string; slot: number; committeeIndex: number}}, allForks.Attestation, VersionMeta >; @@ -850,16 +850,20 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({ - query: {attestation_data_root: toHexString(attestationDataRoot), slot, index}, + writeReq: ({attestationDataRoot, slot, committeeIndex}) => ({ + query: {attestation_data_root: toHexString(attestationDataRoot), slot, committeeIndex}, }), parseReq: ({query}) => ({ attestationDataRoot: fromHexString(query.attestation_data_root), slot: query.slot, - index: query.slot, + committeeIndex: query.slot, }), schema: { - query: {attestation_data_root: Schema.StringRequired, slot: Schema.UintRequired, index: Schema.UintRequired}, + query: { + attestation_data_root: Schema.StringRequired, + slot: Schema.UintRequired, + committeeIndex: Schema.UintRequired, + }, }, }, resp: { diff --git a/packages/api/test/unit/beacon/testData/events.ts b/packages/api/test/unit/beacon/testData/events.ts index 8a7610a26836..7966d0ea3b8a 100644 --- a/packages/api/test/unit/beacon/testData/events.ts +++ b/packages/api/test/unit/beacon/testData/events.ts @@ -31,18 +31,21 @@ export const eventTestData: EventData = { block: "0x9a2fefd2fdb57f74993c7780ea5b9030d2897b615b89f808011ca5aebed54eaf", executionOptimistic: false, }, - [EventType.attestation]: ssz.phase0.Attestation.fromJson({ - aggregation_bits: "0x01", - signature: - "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", - data: { - slot: "1", - index: "1", - beacon_block_root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", - source: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}, - target: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}, - }, - }), + [EventType.attestation]: { + version: ForkName.altair, + data: ssz.phase0.Attestation.fromJson({ + aggregation_bits: "0x01", + signature: + "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", + data: { + slot: "1", + index: "1", + beacon_block_root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + source: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}, + target: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"}, + }, + }), + }, [EventType.voluntaryExit]: ssz.phase0.SignedVoluntaryExit.fromJson({ message: {epoch: "1", validator_index: "1"}, signature: @@ -72,44 +75,47 @@ export const eventTestData: EventData = { "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }, }), - [EventType.attesterSlashing]: ssz.phase0.AttesterSlashing.fromJson({ - attestation_1: { - attesting_indices: ["0", "1"], - data: { - slot: "0", - index: "0", - beacon_block_root: "0x0000000000000000000000000000000000000000000000000000000000000000", - source: { - epoch: "0", - root: "0x0000000000000000000000000000000000000000000000000000000000000000", - }, - target: { - epoch: "0", - root: "0x0000000000000000000000000000000000000000000000000000000000000000", + [EventType.attesterSlashing]: { + version: ForkName.altair, + data: ssz.phase0.AttesterSlashing.fromJson({ + attestation_1: { + attesting_indices: ["0", "1"], + data: { + slot: "0", + index: "0", + beacon_block_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + source: { + epoch: "0", + root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + target: { + epoch: "0", + root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, }, + signature: + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }, - signature: - "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - }, - attestation_2: { - attesting_indices: ["0", "1"], - data: { - slot: "0", - index: "0", - beacon_block_root: "0x0000000000000000000000000000000000000000000000000000000000000000", - source: { - epoch: "0", - root: "0x0000000000000000000000000000000000000000000000000000000000000000", - }, - target: { - epoch: "0", - root: "0x0000000000000000000000000000000000000000000000000000000000000000", + attestation_2: { + attesting_indices: ["0", "1"], + data: { + slot: "0", + index: "0", + beacon_block_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + source: { + epoch: "0", + root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + target: { + epoch: "0", + root: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, }, + signature: + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", }, - signature: - "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - }, - }), + }), + }, [EventType.blsToExecutionChange]: ssz.capella.SignedBLSToExecutionChange.fromJson({ message: { validator_index: "1", diff --git a/packages/api/test/unit/beacon/testData/validator.ts b/packages/api/test/unit/beacon/testData/validator.ts index 3a92beb7ad27..01783241bd2b 100644 --- a/packages/api/test/unit/beacon/testData/validator.ts +++ b/packages/api/test/unit/beacon/testData/validator.ts @@ -103,8 +103,8 @@ export const testData: GenericServerTestCases = { res: {data: ssz.altair.SyncCommitteeContribution.defaultValue()}, }, getAggregatedAttestation: { - args: {attestationDataRoot: ZERO_HASH, slot: 32000}, - res: {data: ssz.phase0.Attestation.defaultValue()}, + args: {attestationDataRoot: ZERO_HASH, slot: 32000, index: 2}, + res: {data: ssz.phase0.Attestation.defaultValue(), meta: {version: ForkName.phase0}}, }, publishAggregateAndProofs: { args: {signedAggregateAndProofs: [ssz.phase0.SignedAggregateAndProof.defaultValue()]}, diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index 77f6b24f28af..3f594fa4c3ae 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -27,12 +27,13 @@ export function getBeaconPoolApi({ async getPoolAttestations({slot, committeeIndex}) { // Already filtered by slot let attestations = chain.aggregatedAttestationPool.getAll(slot); + const fork = chain.config.getForkName(slot ?? attestations[0].data.slot) ?? ForkName.phase0; if (committeeIndex !== undefined) { attestations = attestations.filter((attestation) => committeeIndex === attestation.data.index); } - return {data: attestations}; + return {data: attestations, meta: {version: fork}}; }, async getPoolAttesterSlashings() { diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 7652a67daeb1..9c6ba9a0d40f 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1072,23 +1072,26 @@ export function getValidatorApi({ }; }, - async getAggregatedAttestation({attestationDataRoot, slot}) { + async getAggregatedAttestation({attestationDataRoot, slot, committeeIndex}) { notWhileSyncing(); await waitForSlot(slot); // Must never request for a future slot > currentSlot const dataRootHex = toHex(attestationDataRoot); - const aggregate = chain.attestationPool.getAggregate(slot, dataRootHex); + const aggregate = chain.attestationPool.getAggregate(slot, committeeIndex, dataRootHex); if (!aggregate) { - throw new ApiError(404, `No aggregated attestation for slot=${slot}, dataRoot=${dataRootHex}`); + throw new ApiError( + 404, + `No aggregated attestation for slot=${slot} committeeIndex=${committeeIndex}, dataRoot=${dataRootHex}` + ); } metrics?.production.producedAggregateParticipants.observe(aggregate.aggregationBits.getTrueBitIndexes().length); return { data: aggregate, - version: config.getForkName(slot), + meta: {version: config.getForkName(slot)}, }; }, diff --git a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts index d6cceb9572ae..fde1ac88ace0 100644 --- a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts @@ -30,7 +30,7 @@ import { getBlockRootAtSlot, } from "@lodestar/state-transition"; import {IForkChoice, EpochDifference} from "@lodestar/fork-choice"; -import {toHex, MapDef} from "@lodestar/utils"; +import {toHex, MapDef, assert} from "@lodestar/utils"; import {intersectUint8Arrays, IntersectResult} from "../../util/bitArray.js"; import {pruneBySlot, signatureFromBytesNoCheck} from "./utils.js"; import {InsertOutcome} from "./types.js"; @@ -141,10 +141,8 @@ export class AggregatedAttestationPool { ? // this attestation is added to pool after validation attestation.committeeBits.getSingleTrueBit() : attestation.data.index; - if (committeeIndex === null) { - // this should not happen because attestation should be validated before reaching this - throw Error(`Invalid attestation slot=${slot} committeeIndex=${committeeIndex}`); - } + // this should not happen because attestation should be validated before reaching this + assert.notNull(committeeIndex, "Committee index should not be null in aggregated attestation pool"); let attestationGroup = attestationGroupByIndex.get(committeeIndex); if (!attestationGroup) { attestationGroup = new MatchingDataAttestationGroup(committee, attestation.data); diff --git a/packages/beacon-node/src/chain/opPools/attestationPool.ts b/packages/beacon-node/src/chain/opPools/attestationPool.ts index 38e910753440..7d6cca30056c 100644 --- a/packages/beacon-node/src/chain/opPools/attestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/attestationPool.ts @@ -1,8 +1,8 @@ import {PointFormat, Signature} from "@chainsafe/bls/types"; import bls from "@chainsafe/bls"; import {BitArray} from "@chainsafe/ssz"; -import {Slot, RootHex, allForks} from "@lodestar/types"; -import {MapDef} from "@lodestar/utils"; +import {Slot, RootHex, allForks, isElectraAttestation} from "@lodestar/types"; +import {MapDef, assert} from "@lodestar/utils"; import {IClock} from "../../util/clock.js"; import {InsertOutcome, OpPoolError, OpPoolErrorCode} from "./types.js"; import {pruneBySlot, signatureFromBytesNoCheck} from "./utils.js"; @@ -36,6 +36,8 @@ type AggregateFast = AggregateFastPhase0 | AggregateFastElectra; /** Hex string of DataRoot `TODO` */ type DataRootHex = string; +type CommitteeIndex = number; + /** * A pool of `Attestation` that is specially designed to store "unaggregated" attestations from * the native aggregation scheme. @@ -60,8 +62,8 @@ type DataRootHex = string; * receives and it can be triggered manually. */ export class AttestationPool { - private readonly attestationByRootBySlot = new MapDef>( - () => new Map() + private readonly attestationByRootBySlot = new MapDef>>( + () => new Map>() ); private lowestPermissibleSlot = 0; @@ -117,14 +119,26 @@ export class AttestationPool { throw new OpPoolError({code: OpPoolErrorCode.REACHED_MAX_PER_SLOT}); } + const committeeIndex = isElectraAttestation(attestation) + ? // this attestation is added to pool after validation + attestation.committeeBits.getSingleTrueBit() + : attestation.data.index; + // this should not happen because attestation should be validated before reaching this + assert.notNull(committeeIndex, "Committee index should not be null in attestation pool"); + // Pre-aggregate the contribution with existing items - const aggregate = aggregateByRoot.get(attDataRootHex); + let aggregateByIndex = aggregateByRoot.get(attDataRootHex); + if (aggregateByIndex === undefined) { + aggregateByIndex = new Map(); + aggregateByRoot.set(attDataRootHex, aggregateByIndex); + } + const aggregate = aggregateByIndex.get(committeeIndex); if (aggregate) { // Aggregate mutating return aggregateAttestationInto(aggregate, attestation); } else { // Create new aggregate - aggregateByRoot.set(attDataRootHex, attestationToAggregate(attestation)); + aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation)); return InsertOutcome.NewData; } } @@ -132,8 +146,8 @@ export class AttestationPool { /** * For validator API to get an aggregate */ - getAggregate(slot: Slot, dataRootHex: RootHex): allForks.Attestation | null { - const aggregate = this.attestationByRootBySlot.get(slot)?.get(dataRootHex); + getAggregate(slot: Slot, committeeIndex: CommitteeIndex, dataRootHex: RootHex): allForks.Attestation | null { + const aggregate = this.attestationByRootBySlot.get(slot)?.get(dataRootHex)?.get(committeeIndex); if (!aggregate) { // TODO: Add metric for missing aggregates return null; @@ -166,8 +180,10 @@ export class AttestationPool { for (const aggregateByRoot of aggregateByRoots) { if (aggregateByRoot) { - for (const aggFast of aggregateByRoot.values()) { - attestations.push(fastToAttestation(aggFast)); + for (const aggFastByIndex of aggregateByRoot.values()) { + for (const aggFast of aggFastByIndex.values()) { + attestations.push(fastToAttestation(aggFast)); + } } } } @@ -180,35 +196,13 @@ export class AttestationPool { // - Insert attestations coming from gossip and API /** - * Aggregate a new contribution into `aggregate` mutating it + * Aggregate a new attestation into `aggregate` mutating it */ function aggregateAttestationInto(aggregate: AggregateFast, attestation: allForks.Attestation): InsertOutcome { const bitIndex = attestation.aggregationBits.getSingleTrueBit(); // Should never happen, attestations are verified against this exact condition before - if (bitIndex === null) { - throw Error("Invalid attestation not exactly one bit set"); - } - - if ("committeeBits" in attestation && !("committeeBits" in aggregate)) { - throw Error("Attempt to aggregate electra attestation into phase0 attestation"); - } - - if (!("committeeBits" in attestation) && "committeeBits" in aggregate) { - throw Error("Attempt to aggregate phase0 attestation into electra attestation"); - } - - if ("committeeBits" in attestation) { - // We assume attestation.committeeBits should already be validated in api and gossip handler and should be non-null - const attestationCommitteeIndex = attestation.committeeBits.getSingleTrueBit(); - const aggregateCommitteeIndex = (aggregate as AggregateFastElectra).committeeBits.getSingleTrueBit(); - - if (attestationCommitteeIndex !== aggregateCommitteeIndex) { - throw Error( - `Committee index mismatched: attestation ${attestationCommitteeIndex} aggregate ${aggregateCommitteeIndex}` - ); - } - } + assert.notNull(bitIndex, "Invalid attestation in pool, not exactly one bit set"); if (aggregate.aggregationBits.get(bitIndex) === true) { return InsertOutcome.AlreadyKnown; @@ -226,7 +220,7 @@ function aggregateAttestationInto(aggregate: AggregateFast, attestation: allFork * Format `contribution` into an efficient `aggregate` to add more contributions in with aggregateContributionInto() */ function attestationToAggregate(attestation: allForks.Attestation): AggregateFast { - if ("committeeBits" in attestation) { + if (isElectraAttestation(attestation)) { return { data: attestation.data, // clone because it will be mutated @@ -247,18 +241,5 @@ function attestationToAggregate(attestation: allForks.Attestation): AggregateFas * Unwrap AggregateFast to phase0.Attestation */ function fastToAttestation(aggFast: AggregateFast): allForks.Attestation { - if ("committeeBits" in aggFast) { - return { - data: aggFast.data, - aggregationBits: aggFast.aggregationBits, - committeeBits: aggFast.committeeBits, - signature: aggFast.signature.toBytes(PointFormat.compressed), - }; - } else { - return { - data: aggFast.data, - aggregationBits: aggFast.aggregationBits, - signature: aggFast.signature.toBytes(PointFormat.compressed), - }; - } + return {...aggFast, signature: aggFast.signature.toBytes(PointFormat.compressed)}; } diff --git a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts index 7950466570ce..a1d7135716ce 100644 --- a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts @@ -82,7 +82,7 @@ async function validateAggregateAndProof( throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET}); } // [REJECT] aggregate.data.index == 0 - if (attData.index === 0) { + if (attData.index !== 0) { throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}); } } else { diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index a186b1325396..87e8c2a6b2bd 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -44,7 +44,7 @@ export type AttestationValidationResult = { export type AttestationOrBytes = ApiAttestation | GossipAttestation; /** attestation from api */ -export type ApiAttestation = {attestation: phase0.Attestation; serializedData: null}; // TODO Electra: add new attestation type +export type ApiAttestation = {attestation: phase0.Attestation; serializedData: null}; /** attestation from gossip */ export type GossipAttestation = { @@ -298,7 +298,7 @@ async function validateGossipAttestationNoSignatureCheck( } // [REJECT] aggregate.data.index == 0 - if (attData.index === 0) { + if (attData.index !== 0) { throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}); } } else { diff --git a/packages/state-transition/test/perf/epoch/epochAltair.test.ts b/packages/state-transition/test/perf/epoch/epochAltair.test.ts index 273353d8632b..6c43151cc137 100644 --- a/packages/state-transition/test/perf/epoch/epochAltair.test.ts +++ b/packages/state-transition/test/perf/epoch/epochAltair.test.ts @@ -120,7 +120,7 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - altair processRegistryUpdates`, beforeEach: () => stateOg.value.clone(), - fn: (state) => processRegistryUpdates(state, cache.value), + fn: (state) => processRegistryUpdates(ForkSeq.altair, state, cache.value), }); // TODO: Needs a better state to test with, current does not include enough actions: 39.985 us/op @@ -141,7 +141,7 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - altair processEffectiveBalanceUpdates`, beforeEach: () => stateOg.value.clone(), - fn: (state) => processEffectiveBalanceUpdates(state, cache.value), + fn: (state) => processEffectiveBalanceUpdates(ForkSeq.altair, state, cache.value), }); itBench({ diff --git a/packages/state-transition/test/perf/epoch/epochCapella.test.ts b/packages/state-transition/test/perf/epoch/epochCapella.test.ts index eeaf8bfc5400..5b8300df3f18 100644 --- a/packages/state-transition/test/perf/epoch/epochCapella.test.ts +++ b/packages/state-transition/test/perf/epoch/epochCapella.test.ts @@ -99,7 +99,7 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - capella processRegistryUpdates`, beforeEach: () => stateOg.value.clone(), - fn: (state) => processRegistryUpdates(state, cache.value), + fn: (state) => processRegistryUpdates(ForkSeq.capella, state, cache.value), }); // TODO: Needs a better state to test with, current does not include enough actions: 39.985 us/op @@ -120,7 +120,7 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - capella processEffectiveBalanceUpdates`, beforeEach: () => stateOg.value.clone(), - fn: (state) => processEffectiveBalanceUpdates(state, cache.value), + fn: (state) => processEffectiveBalanceUpdates(ForkSeq.capella, state, cache.value), }); itBench({ diff --git a/packages/state-transition/test/perf/epoch/epochPhase0.test.ts b/packages/state-transition/test/perf/epoch/epochPhase0.test.ts index 4e43634b1669..411577878102 100644 --- a/packages/state-transition/test/perf/epoch/epochPhase0.test.ts +++ b/packages/state-transition/test/perf/epoch/epochPhase0.test.ts @@ -102,7 +102,7 @@ function benchmarkPhase0EpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - phase0 processRegistryUpdates`, beforeEach: () => stateOg.value.clone(), - fn: (state) => processRegistryUpdates(state, cache.value), + fn: (state) => processRegistryUpdates(ForkSeq.phase0, state, cache.value), }); // TODO: Needs a better state to test with, current does not include enough actions: 39.985 us/op @@ -123,7 +123,7 @@ function benchmarkPhase0EpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - phase0 processEffectiveBalanceUpdates`, beforeEach: () => stateOg.value.clone(), - fn: (state) => processEffectiveBalanceUpdates(state, cache.value), + fn: (state) => processEffectiveBalanceUpdates(ForkSeq.phase0, state, cache.value), }); itBench({ diff --git a/packages/state-transition/test/perf/epoch/processEffectiveBalanceUpdates.test.ts b/packages/state-transition/test/perf/epoch/processEffectiveBalanceUpdates.test.ts index 0fb1d448142f..d94daac9e59b 100644 --- a/packages/state-transition/test/perf/epoch/processEffectiveBalanceUpdates.test.ts +++ b/packages/state-transition/test/perf/epoch/processEffectiveBalanceUpdates.test.ts @@ -1,6 +1,7 @@ import {itBench} from "@dapplion/benchmark"; import {ssz} from "@lodestar/types"; import {config} from "@lodestar/config/default"; +import {ForkSeq} from "@lodestar/params"; import {beforeProcessEpoch, CachedBeaconStateAllForks, EpochTransitionCache} from "../../../src/index.js"; import {processEffectiveBalanceUpdates} from "../../../src/epoch/processEffectiveBalanceUpdates.js"; import {numValidators} from "../util.js"; @@ -35,7 +36,7 @@ describe("phase0 processEffectiveBalanceUpdates", () => { minRuns: 5, // Worst case is very slow before: () => getEffectiveBalanceTestData(vc, changeRatio), beforeEach: ({state, cache}) => ({state: state.clone(), cache}), - fn: ({state, cache}) => processEffectiveBalanceUpdates(state, cache), + fn: ({state, cache}) => processEffectiveBalanceUpdates(ForkSeq.phase0, state, cache), }); } }); diff --git a/packages/state-transition/test/perf/epoch/processRegistryUpdates.test.ts b/packages/state-transition/test/perf/epoch/processRegistryUpdates.test.ts index ccfd2405a665..2d57de44f8ee 100644 --- a/packages/state-transition/test/perf/epoch/processRegistryUpdates.test.ts +++ b/packages/state-transition/test/perf/epoch/processRegistryUpdates.test.ts @@ -1,4 +1,5 @@ import {itBench} from "@dapplion/benchmark"; +import {ForkSeq} from "@lodestar/params"; import {beforeProcessEpoch, CachedBeaconStateAllForks, EpochTransitionCache} from "../../../src/index.js"; import {processRegistryUpdates} from "../../../src/epoch/processRegistryUpdates.js"; import {generatePerfTestCachedStatePhase0, numValidators} from "../util.js"; @@ -62,7 +63,7 @@ describe("phase0 processRegistryUpdates", () => { noThreshold: notTrack, before: () => getRegistryUpdatesTestData(vc, lengths), beforeEach: async ({state, cache}) => ({state: state.clone(), cache}), - fn: ({state, cache}) => processRegistryUpdates(state, cache), + fn: ({state, cache}) => processRegistryUpdates(ForkSeq.phase0, state, cache), }); } }); diff --git a/packages/utils/src/assert.ts b/packages/utils/src/assert.ts index aa86161cca44..91612b0e6407 100644 --- a/packages/utils/src/assert.ts +++ b/packages/utils/src/assert.ts @@ -21,6 +21,18 @@ export const assert = { } }, + /** + * Assert not null + * ``` + * actual !== null + * ``` + */ + notNull(actual: T | null, message?: string): asserts actual is T { + if (!(actual !== null)) { + throw new AssertionError(`${message || "Expected value to be not null"}`); + } + }, + /** * Assert less than or equal * ```js diff --git a/packages/utils/test/unit/assert.test.ts b/packages/utils/test/unit/assert.test.ts index 0555bcbd01a0..3b413efa11be 100644 --- a/packages/utils/test/unit/assert.test.ts +++ b/packages/utils/test/unit/assert.test.ts @@ -20,8 +20,18 @@ describe("assert", () => { }); }); + describe("notNull with custom message", () => { + it("Should not throw error with not null value", () => { + expect(() => assert.notNull(0)).not.toThrow(); + expect(() => assert.notNull("")).not.toThrow(); + }); + it("Should throw with null value", () => { + expect(() => assert.notNull(null, "something must not be null")).toThrow("something must not be null"); + }); + }); + const cases: { - op: keyof Omit; + op: keyof Omit; args: [number, number]; ok: boolean; }[] = [ diff --git a/packages/validator/src/services/attestation.ts b/packages/validator/src/services/attestation.ts index 39c3cfac641d..8f5228e3a10e 100644 --- a/packages/validator/src/services/attestation.ts +++ b/packages/validator/src/services/attestation.ts @@ -128,7 +128,7 @@ export class AttestationService { // Then download, sign and publish a `SignedAggregateAndProof` for each // validator that is elected to aggregate for this `slot` and `committeeIndex`. - await this.produceAndPublishAggregates(attestation, dutiesSameCommittee); + await this.produceAndPublishAggregates(attestation, index, dutiesSameCommittee); } private async runAttestationTasksGrouped( @@ -154,7 +154,7 @@ export class AttestationService { await Promise.all( Array.from(dutiesByCommitteeIndex.entries()).map(([index, dutiesSameCommittee]) => { const attestationData: phase0.AttestationData = {...attestationNoCommittee, index}; - return this.produceAndPublishAggregates(attestationData, dutiesSameCommittee); + return this.produceAndPublishAggregates(attestationData, index, dutiesSameCommittee); }) ); } @@ -245,9 +245,10 @@ export class AttestationService { */ private async produceAndPublishAggregates( attestation: phase0.AttestationData, + committeeIndex: number, duties: AttDutyAndProof[] ): Promise { - const logCtx = {slot: attestation.slot, index: attestation.index}; + const logCtx = {slot: attestation.slot, index: committeeIndex}; // No validator is aggregator, skip if (duties.every(({selectionProof}) => selectionProof === null)) { @@ -258,6 +259,7 @@ export class AttestationService { const res = await this.api.validator.getAggregatedAttestation({ attestationDataRoot: ssz.phase0.AttestationData.hashTreeRoot(attestation), slot: attestation.slot, + committeeIndex, }); const aggregate = res.value(); this.metrics?.numParticipantsInAggregate.observe(aggregate.aggregationBits.getTrueBitIndexes().length); diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 1d5d73377946..d7a945998f33 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -20,6 +20,7 @@ import { DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF, DOMAIN_APPLICATION_BUILDER, ForkSeq, + MAX_COMMITTEES_PER_SLOT, } from "@lodestar/params"; import { allForks, @@ -528,7 +529,7 @@ export class ValidatorStore { return { aggregationBits: BitArray.fromSingleBit(duty.committeeLength, duty.validatorCommitteeIndex), data: attestationData, - committeeBits: BitArray.fromSingleBit(duty.committeesAtSlot, duty.committeeIndex), + committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, duty.committeeIndex), signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage), } as electra.Attestation; } else { From 6c34113624cd9e6ac2297dc7dbf0bd4e79c5a1c3 Mon Sep 17 00:00:00 2001 From: g11tech Date: Wed, 8 May 2024 17:11:08 +0530 Subject: [PATCH 14/51] feat: update engineapi endpoints to v4 (#6747) --- packages/beacon-node/src/execution/engine/http.ts | 6 +++--- packages/beacon-node/src/execution/engine/mock.ts | 4 ++-- packages/beacon-node/src/execution/engine/types.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/beacon-node/src/execution/engine/http.ts b/packages/beacon-node/src/execution/engine/http.ts index 140663aef6ac..be91d4509236 100644 --- a/packages/beacon-node/src/execution/engine/http.ts +++ b/packages/beacon-node/src/execution/engine/http.ts @@ -177,7 +177,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { ): Promise { const method = ForkSeq[fork] >= ForkSeq.electra - ? "engine_newPayloadV6110" + ? "engine_newPayloadV4" : ForkSeq[fork] >= ForkSeq.deneb ? "engine_newPayloadV3" : ForkSeq[fork] >= ForkSeq.capella @@ -198,7 +198,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { const serializedVersionedHashes = serializeVersionedHashes(versionedHashes); const parentBeaconBlockRoot = serializeBeaconBlockRoot(parentBlockRoot); - const method = ForkSeq[fork] >= ForkSeq.electra ? "engine_newPayloadV6110" : "engine_newPayloadV3"; + const method = ForkSeq[fork] >= ForkSeq.electra ? "engine_newPayloadV4" : "engine_newPayloadV3"; engineRequest = { method, params: [serializedExecutionPayload, serializedVersionedHashes, parentBeaconBlockRoot], @@ -373,7 +373,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { }> { const method = ForkSeq[fork] >= ForkSeq.electra - ? "engine_getPayloadV6110" + ? "engine_getPayloadV4" : ForkSeq[fork] >= ForkSeq.deneb ? "engine_getPayloadV3" : ForkSeq[fork] >= ForkSeq.capella diff --git a/packages/beacon-node/src/execution/engine/mock.ts b/packages/beacon-node/src/execution/engine/mock.ts index 83b63bc01c7c..aa15a2bc9532 100644 --- a/packages/beacon-node/src/execution/engine/mock.ts +++ b/packages/beacon-node/src/execution/engine/mock.ts @@ -89,14 +89,14 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { engine_newPayloadV1: this.notifyNewPayload.bind(this), engine_newPayloadV2: this.notifyNewPayload.bind(this), engine_newPayloadV3: this.notifyNewPayload.bind(this), - engine_newPayloadV6110: this.notifyNewPayload.bind(this), + engine_newPayloadV4: this.notifyNewPayload.bind(this), engine_forkchoiceUpdatedV1: this.notifyForkchoiceUpdate.bind(this), engine_forkchoiceUpdatedV2: this.notifyForkchoiceUpdate.bind(this), engine_forkchoiceUpdatedV3: this.notifyForkchoiceUpdate.bind(this), engine_getPayloadV1: this.getPayload.bind(this), engine_getPayloadV2: this.getPayload.bind(this), engine_getPayloadV3: this.getPayload.bind(this), - engine_getPayloadV6110: this.getPayload.bind(this), + engine_getPayloadV4: this.getPayload.bind(this), engine_getPayloadBodiesByHashV1: this.getPayloadBodiesByHash.bind(this), engine_getPayloadBodiesByRangeV1: this.getPayloadBodiesByRange.bind(this), }; diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index c5d798c9bf5e..22d6c0a7116a 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -28,7 +28,7 @@ export type EngineApiRpcParamTypes = { engine_newPayloadV1: [ExecutionPayloadRpc]; engine_newPayloadV2: [ExecutionPayloadRpc]; engine_newPayloadV3: [ExecutionPayloadRpc, VersionedHashesRpc, DATA]; - engine_newPayloadV6110: [ExecutionPayloadRpc, VersionedHashesRpc, DATA]; + engine_newPayloadV4: [ExecutionPayloadRpc, VersionedHashesRpc, DATA]; /** * 1. Object - Payload validity status with respect to the consensus rules: * - blockHash: DATA, 32 Bytes - block hash value of the payload @@ -52,7 +52,7 @@ export type EngineApiRpcParamTypes = { engine_getPayloadV1: [QUANTITY]; engine_getPayloadV2: [QUANTITY]; engine_getPayloadV3: [QUANTITY]; - engine_getPayloadV6110: [QUANTITY]; + engine_getPayloadV4: [QUANTITY]; /** * 1. Array of DATA - Array of block_hash field values of the ExecutionPayload structure @@ -80,7 +80,7 @@ export type EngineApiRpcReturnTypes = { engine_newPayloadV1: PayloadStatus; engine_newPayloadV2: PayloadStatus; engine_newPayloadV3: PayloadStatus; - engine_newPayloadV6110: PayloadStatus; + engine_newPayloadV4: PayloadStatus; engine_forkchoiceUpdatedV1: { payloadStatus: PayloadStatus; payloadId: QUANTITY | null; @@ -99,7 +99,7 @@ export type EngineApiRpcReturnTypes = { engine_getPayloadV1: ExecutionPayloadRpc; engine_getPayloadV2: ExecutionPayloadResponse; engine_getPayloadV3: ExecutionPayloadResponse; - engine_getPayloadV6110: ExecutionPayloadResponse; + engine_getPayloadV4: ExecutionPayloadResponse; engine_getPayloadBodiesByHashV1: (ExecutionPayloadBodyRpc | null)[]; From a046aef603d242b13060474f3d45d96391b1bf0d Mon Sep 17 00:00:00 2001 From: NC Date: Wed, 8 May 2024 18:12:36 +0300 Subject: [PATCH 15/51] feat: rename deposit receipt to deposit request for Pectra (#6748) * Rename receipt to request * Remove stats.html --- packages/beacon-node/src/chain/chain.ts | 2 +- .../src/eth1/eth1DepositDataTracker.ts | 4 +- .../src/execution/engine/interface.ts | 4 +- .../src/execution/engine/payloadIdCache.ts | 2 +- .../beacon-node/src/execution/engine/types.ts | 40 +++++++++---------- .../test/sim/electra-interop.test.ts | 28 ++++++------- .../test/spec/presets/operations.test.ts | 6 +-- .../test/unit/executionEngine/http.test.ts | 8 ++-- packages/beacon-node/test/utils/state.ts | 2 +- packages/light-client/src/spec/utils.ts | 6 +-- .../src/block/processDeposit.ts | 6 +-- ...sitReceipt.ts => processDepositRequest.ts} | 10 ++--- .../src/block/processOperations.ts | 8 ++-- .../src/slot/upgradeStateToElectra.ts | 6 +-- packages/state-transition/src/util/deposit.ts | 4 +- .../state-transition/src/util/execution.ts | 4 +- packages/state-transition/src/util/genesis.ts | 2 +- .../test/unit/util/deposit.test.ts | 4 +- packages/types/src/electra/sszTypes.ts | 12 +++--- packages/types/src/electra/types.ts | 4 +- 20 files changed, 81 insertions(+), 81 deletions(-) rename packages/state-transition/src/block/{processDepositReceipt.ts => processDepositRequest.ts} (55%) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 25b72c57f769..f8007d12aa8b 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -1093,7 +1093,7 @@ export class BeaconChain implements IBeaconChain { // Will resolve this later // if (cpEpoch >= (this.config.ELECTRA_FORK_EPOCH ?? Infinity)) { // // finalizedState can be safely casted to Electra state since cp is already post-Electra - // if (finalizedState.eth1DepositIndex >= (finalizedState as CachedBeaconStateElectra).depositReceiptsStartIndex) { + // if (finalizedState.eth1DepositIndex >= (finalizedState as CachedBeaconStateElectra).depositRequestsStartIndex) { // // Signal eth1 to stop polling eth1Data // this.eth1.stopPollingEth1Data(); // } diff --git a/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts b/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts index c0b3ab35a73a..d0578718f29f 100644 --- a/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts +++ b/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts @@ -129,9 +129,9 @@ export class Eth1DepositDataTracker { async getEth1DataAndDeposits(state: CachedBeaconStateAllForks): Promise { if ( state.epochCtx.isAfterElectra() && - state.eth1DepositIndex >= (state as CachedBeaconStateElectra).depositReceiptsStartIndex + state.eth1DepositIndex >= (state as CachedBeaconStateElectra).depositRequestsStartIndex ) { - // No need to poll eth1Data since Electra deprecates the mechanism after depositReceiptsStartIndex is reached + // No need to poll eth1Data since Electra deprecates the mechanism after depositRequestsStartIndex is reached return {eth1Data: state.eth1Data, deposits: []}; } const eth1Data = this.forcedEth1DataVote ?? (await this.getEth1Data(state)); diff --git a/packages/beacon-node/src/execution/engine/interface.ts b/packages/beacon-node/src/execution/engine/interface.ts index c303ee6d6bf3..0ffeeb589df2 100644 --- a/packages/beacon-node/src/execution/engine/interface.ts +++ b/packages/beacon-node/src/execution/engine/interface.ts @@ -3,10 +3,10 @@ import {KZGCommitment, Blob, KZGProof} from "@lodestar/types/deneb"; import {Root, RootHex, allForks, capella, Wei} from "@lodestar/types"; import {DATA} from "../../eth1/provider/utils.js"; -import {PayloadIdCache, PayloadId, WithdrawalV1, DepositReceiptV1} from "./payloadIdCache.js"; +import {PayloadIdCache, PayloadId, WithdrawalV1, DepositRequestV1} from "./payloadIdCache.js"; import {ExecutionPayloadBody} from "./types.js"; -export {PayloadIdCache, type PayloadId, type WithdrawalV1, type DepositReceiptV1}; +export {PayloadIdCache, type PayloadId, type WithdrawalV1, type DepositRequestV1}; export enum ExecutionPayloadStatus { /** given payload is valid */ diff --git a/packages/beacon-node/src/execution/engine/payloadIdCache.ts b/packages/beacon-node/src/execution/engine/payloadIdCache.ts index b5fe3d33e267..f79c28582459 100644 --- a/packages/beacon-node/src/execution/engine/payloadIdCache.ts +++ b/packages/beacon-node/src/execution/engine/payloadIdCache.ts @@ -18,7 +18,7 @@ export type WithdrawalV1 = { amount: QUANTITY; }; -export type DepositReceiptV1 = { +export type DepositRequestV1 = { pubkey: DATA; withdrawalCredentials: DATA; amount: QUANTITY; diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index 22d6c0a7116a..71e8e2fc77a5 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, ExecutionLayerWithdrawalRequestV1} from "./payloadIdCache.js"; +import {WithdrawalV1, DepositRequestV1, ExecutionLayerWithdrawalRequestV1} from "./payloadIdCache.js"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -120,14 +120,14 @@ export type ExecutionPayloadBodyRpc = { withdrawals: WithdrawalV1[] | null | undefined; // currently there is a discepancy between EL and CL field name references for deposit requests // its likely CL receipt will be renamed to requests - depositRequests: DepositReceiptV1[] | null | undefined; + depositRequests: DepositRequestV1[] | null | undefined; withdrawalRequests: ExecutionLayerWithdrawalRequestV1[] | null | undefined; }; export type ExecutionPayloadBody = { transactions: bellatrix.Transaction[]; withdrawals: capella.Withdrawals | null; - depositReceipts: electra.DepositReceipts | null; + depositRequests: electra.DepositRequests | null; withdrawalRequests: electra.ExecutionLayerWithdrawalRequests | null; }; @@ -150,7 +150,7 @@ export type ExecutionPayloadRpc = { blobGasUsed?: QUANTITY; // DENEB excessBlobGas?: QUANTITY; // DENEB parentBeaconBlockRoot?: QUANTITY; // DENEB - depositRequests?: DepositReceiptRpc[]; // ELECTRA + depositRequests?: DepositRequestRpc[]; // ELECTRA withdrawalRequests?: ExecutionLayerWithdrawalRequestRpc[]; // ELECTRA }; @@ -161,7 +161,7 @@ export type WithdrawalRpc = { amount: QUANTITY; }; -export type DepositReceiptRpc = DepositReceiptV1; +export type DepositRequestRpc = DepositRequestV1; export type ExecutionLayerWithdrawalRequestRpc = ExecutionLayerWithdrawalRequestV1; export type VersionedHashesRpc = DATA[]; @@ -215,10 +215,10 @@ export function serializeExecutionPayload(fork: ForkName, data: allForks.Executi payload.excessBlobGas = numToQuantity(excessBlobGas); } - // ELECTRA adds depositReceipts/depositRequests to the ExecutionPayload + // ELECTRA adds depositRequests/depositRequests to the ExecutionPayload if (ForkSeq[fork] >= ForkSeq.electra) { - const {depositReceipts, withdrawalRequests} = data as electra.ExecutionPayload; - payload.depositRequests = depositReceipts.map(serializeDepositReceipt); + const {depositRequests, withdrawalRequests} = data as electra.ExecutionPayload; + payload.depositRequests = depositRequests.map(serializeDepositRequest); payload.withdrawalRequests = withdrawalRequests.map(serializeExecutionLayerWithdrawalRequest); } @@ -308,7 +308,7 @@ export function parseExecutionPayload( } if (ForkSeq[fork] >= ForkSeq.electra) { - // electra adds depositRequests/depositReceipts + // electra adds depositRequests/depositRequests const {depositRequests, withdrawalRequests} = data; // Geth can also reply with null if (depositRequests == null) { @@ -316,7 +316,7 @@ export function parseExecutionPayload( `depositRequests missing for ${fork} >= electra executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}` ); } - (executionPayload as electra.ExecutionPayload).depositReceipts = depositRequests.map(deserializeDepositReceipt); + (executionPayload as electra.ExecutionPayload).depositRequests = depositRequests.map(deserializeDepositRequest); if (withdrawalRequests == null) { throw Error( @@ -394,24 +394,24 @@ export function deserializeWithdrawal(serialized: WithdrawalRpc): capella.Withdr } as capella.Withdrawal; } -export function serializeDepositReceipt(depositReceipt: electra.DepositReceipt): DepositReceiptRpc { +export function serializeDepositRequest(depositRequest: electra.DepositRequest): DepositRequestRpc { return { - pubkey: bytesToData(depositReceipt.pubkey), - withdrawalCredentials: bytesToData(depositReceipt.withdrawalCredentials), - amount: numToQuantity(depositReceipt.amount), - signature: bytesToData(depositReceipt.signature), - index: numToQuantity(depositReceipt.index), + pubkey: bytesToData(depositRequest.pubkey), + withdrawalCredentials: bytesToData(depositRequest.withdrawalCredentials), + amount: numToQuantity(depositRequest.amount), + signature: bytesToData(depositRequest.signature), + index: numToQuantity(depositRequest.index), }; } -export function deserializeDepositReceipt(serialized: DepositReceiptRpc): electra.DepositReceipt { +export function deserializeDepositRequest(serialized: DepositRequestRpc): electra.DepositRequest { return { pubkey: dataToBytes(serialized.pubkey, 48), withdrawalCredentials: dataToBytes(serialized.withdrawalCredentials, 32), amount: quantityToNum(serialized.amount), signature: dataToBytes(serialized.signature, 96), index: quantityToNum(serialized.index), - } as electra.DepositReceipt; + } as electra.DepositRequest; } export function serializeExecutionLayerWithdrawalRequest( @@ -439,7 +439,7 @@ export function deserializeExecutionPayloadBody(data: ExecutionPayloadBodyRpc | ? { transactions: data.transactions.map((tran) => dataToBytes(tran, null)), withdrawals: data.withdrawals ? data.withdrawals.map(deserializeWithdrawal) : null, - depositReceipts: data.depositRequests ? data.depositRequests.map(deserializeDepositReceipt) : null, + depositRequests: data.depositRequests ? data.depositRequests.map(deserializeDepositRequest) : null, withdrawalRequests: data.withdrawalRequests ? data.withdrawalRequests.map(deserializeExecutionLayerWithdrawalRequest) : null, @@ -452,7 +452,7 @@ export function serializeExecutionPayloadBody(data: ExecutionPayloadBody | null) ? { transactions: data.transactions.map((tran) => bytesToData(tran)), withdrawals: data.withdrawals ? data.withdrawals.map(serializeWithdrawal) : null, - depositRequests: data.depositReceipts ? data.depositReceipts.map(serializeDepositReceipt) : null, + depositRequests: data.depositRequests ? data.depositRequests.map(serializeDepositRequest) : null, withdrawalRequests: data.withdrawalRequests ? data.withdrawalRequests.map(serializeExecutionLayerWithdrawalRequest) : null, diff --git a/packages/beacon-node/test/sim/electra-interop.test.ts b/packages/beacon-node/test/sim/electra-interop.test.ts index 29483b249c85..c503052a74f4 100644 --- a/packages/beacon-node/test/sim/electra-interop.test.ts +++ b/packages/beacon-node/test/sim/electra-interop.test.ts @@ -72,7 +72,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { } }); - it("Send and get payloads with depositReceipts to/from EL", async () => { + it("Send and get payloads with depositRequests to/from EL", async () => { const {elClient, tearDownCallBack} = await runEL( {...elSetupConfig, mode: ELStartMode.PostMerge, genesisTemplate: "electra.tmpl"}, {...elRunOptions, ttd: BigInt(0)}, @@ -111,7 +111,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { // 2. Send raw deposit transaction A and B. tx A is to be imported via newPayload, tx B is to be included in payload via getPayload const depositTransactionA = "0x02f9021c8217de808459682f008459682f0e830271009442424242424242424242424242424242424242428901bc16d674ec800000b901a422895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120749715de5d1226545c6b3790f515d551a5cc5bf1d49c87a696860554d2fc4f14000000000000000000000000000000000000000000000000000000000000003096a96086cff07df17668f35f7418ef8798079167e3f4f9b72ecde17b28226137cf454ab1dd20ef5d924786ab3483c2f9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020003f5102dabe0a27b1746098d1dc17a5d3fbd478759fea9287e4e419b3c3cef20000000000000000000000000000000000000000000000000000000000000060b1acdb2c4d3df3f1b8d3bfd33421660df358d84d78d16c4603551935f4b67643373e7eb63dcb16ec359be0ec41fee33b03a16e80745f2374ff1d3c352508ac5d857c6476d3c3bcf7e6ca37427c9209f17be3af5264c0e2132b3dd1156c28b4e9c080a09f597089338d7f44f5c59f8230bb38f243849228a8d4e9d2e2956e6050f5b2c7a076486996c7e62802b8f95eee114783e4b403fd11093ba96286ff42c595f24452"; - const depositReceiptA = { + const depositRequestA = { amount: 32000000000, index: 0, pubkey: dataToBytes( @@ -127,7 +127,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { const depositTransactionB = "0x02f9021c8217de018459682f008459682f0e830271009442424242424242424242424242424242424242428901bc16d674ec800000b901a422895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120a18b4c7cab0afa273ea9504904521ea8421a4e32740b7611bd3d5095ca99f0cb0000000000000000000000000000000000000000000000000000000000000030a5c85a60ba2905c215f6a12872e62b1ee037051364244043a5f639aa81b04a204c55e7cc851f29c7c183be253ea1510b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020001db70c485b6264692f26b8aeaab5b0c384180df8e2184a21a808a3ec8e86ca00000000000000000000000000000000000000000000000000000000000000609561731785b48cf1886412234531e4940064584463e96ac63a1a154320227e333fb51addc4a89b7e0d3f862d7c1fd4ea03bd8eb3d8806f1e7daf591cbbbb92b0beb74d13c01617f22c5026b4f9f9f294a8a7c32db895de3b01bee0132c9209e1c001a0644e0a763a34b4bfb9f56a677857b57fcf15e3db57e2f57060e92084f75f3d82a018ba8eaacbd8e6f6917675b1d0362b12ca82850ca8ef9c010430760c2b2e0cb5"; - const depositReceiptB = { + const depositRequestB = { amount: 32000000000, index: 1, pubkey: dataToBytes( @@ -168,7 +168,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { excessBlobGas: 0n, transactions: [dataToBytes(depositTransactionA, null)], withdrawals: [], - depositReceipts: [depositReceiptA], + depositRequests: [depositRequestA], blockNumber: 1, blockHash: dataToBytes(newPayloadBlockHash, 32), receiptsRoot: dataToBytes("0x79ee3424eb720a3ad4b1c5a372bb8160580cbe4d893778660f34213c685627a9", 32), @@ -205,7 +205,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { ); if (!payloadId2) throw Error("InvalidPayloadId"); - // 5. Get the payload. Check depositReceipts field contains deposit + // 5. Get the payload. Check depositRequests field contains deposit // Wait a bit first for besu to pick up tx from the tx pool. await sleep(1000); const payloadAndBlockValue = await executionEngine.getPayload(ForkName.electra, payloadId2); @@ -221,16 +221,16 @@ describe("executionEngine / ExecutionEngineHttp", function () { } } - if (payload.depositReceipts.length !== 1) { - throw Error(`Number of depositReceipts mismatched. Expected: 1, actual: ${payload.depositReceipts.length}`); + if (payload.depositRequests.length !== 1) { + throw Error(`Number of depositRequests mismatched. Expected: 1, actual: ${payload.depositRequests.length}`); } - const actualDepositReceipt = payload.depositReceipts[0]; + const actualDepositRequest = payload.depositRequests[0]; assert.deepStrictEqual( - actualDepositReceipt, - depositReceiptB, - `Deposit receipts mismatched. Expected: ${JSON.stringify(depositReceiptB)}, actual: ${JSON.stringify( - actualDepositReceipt + actualDepositRequest, + depositRequestB, + `Deposit receipts mismatched. Expected: ${JSON.stringify(depositRequestB)}, actual: ${JSON.stringify( + actualDepositRequest )}` ); }); @@ -432,8 +432,8 @@ describe("executionEngine / ExecutionEngineHttp", function () { throw Error("Historical validator length for epoch 1 or 2 is not dropped properly"); } - if (headState.depositReceiptsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { - throw Error("state.depositReceiptsStartIndex is not set upon processing new deposit receipt"); + if (headState.depositRequestsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { + throw Error("state.depositRequestsStartIndex is not set upon processing new deposit receipt"); } // wait for 1 slot to print current epoch stats diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index a96d38298542..0e6fdafd2256 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -57,9 +57,9 @@ const operationFns: Record> = blockFns.processDeposit(fork, state, testCase.deposit); }, - deposit_receipt: (state, testCase: {deposit_receipt: electra.DepositReceipt}) => { + deposit_receipt: (state, testCase: {deposit_receipt: electra.DepositRequest}) => { const fork = state.config.getForkSeq(state.slot); - blockFns.processDepositReceipt(fork, state as CachedBeaconStateElectra, testCase.deposit_receipt); + blockFns.processDepositRequest(fork, state as CachedBeaconStateElectra, testCase.deposit_receipt); }, proposer_slashing: (state, testCase: {proposer_slashing: phase0.ProposerSlashing}) => { @@ -127,7 +127,7 @@ const operations: TestRunnerFn = (fork, block: ssz[fork].BeaconBlock, body: ssz[fork].BeaconBlockBody, deposit: ssz.phase0.Deposit, - deposit_receipt: ssz.electra.DepositReceipt, + deposit_receipt: ssz.electra.DepositRequest, proposer_slashing: ssz.phase0.ProposerSlashing, voluntary_exit: ssz.phase0.SignedVoluntaryExit, // Altair diff --git a/packages/beacon-node/test/unit/executionEngine/http.test.ts b/packages/beacon-node/test/unit/executionEngine/http.test.ts index 5721b9ce5ad1..6d3cb17335e6 100644 --- a/packages/beacon-node/test/unit/executionEngine/http.test.ts +++ b/packages/beacon-node/test/unit/executionEngine/http.test.ts @@ -188,7 +188,7 @@ describe("ExecutionEngine / http", () => { amount: "0x7b", }, ], - depositReceipts: null, // depositReceipts is null pre-electra + depositRequests: null, // depositRequests is null pre-electra withdrawalRequests: null, }, null, // null returned for missing blocks @@ -198,7 +198,7 @@ describe("ExecutionEngine / http", () => { "0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174", ], withdrawals: null, // withdrawals is null pre-capella - depositReceipts: null, // depositReceipts is null pre-electra + depositRequests: null, // depositRequests is null pre-electra withdrawalRequests: null, }, ], @@ -247,7 +247,7 @@ describe("ExecutionEngine / http", () => { amount: "0x7b", }, ], - depositReceipts: null, // depositReceipts is null pre-electra + depositRequests: null, // depositRequests is null pre-electra withdrawalRequests: null, }, null, // null returned for missing blocks @@ -257,7 +257,7 @@ describe("ExecutionEngine / http", () => { "0xb084c10440f05f5a23a55d1d7ebcb1b3892935fb56f23cdc9a7f42c348eed174", ], withdrawals: null, // withdrawals is null pre-capella - depositReceipts: null, // depositReceipts is null pre-electra + depositRequests: null, // depositRequests is null pre-electra withdrawalRequests: null, }, ], diff --git a/packages/beacon-node/test/utils/state.ts b/packages/beacon-node/test/utils/state.ts index 04e1dc710b5e..70b50c86004b 100644 --- a/packages/beacon-node/test/utils/state.ts +++ b/packages/beacon-node/test/utils/state.ts @@ -97,7 +97,7 @@ export function generateState( if (forkSeq >= ForkSeq.electra) { const stateElectra = state as electra.BeaconState; - stateElectra.depositReceiptsStartIndex = 2023n; + stateElectra.depositRequestsStartIndex = 2023n; stateElectra.latestExecutionPayloadHeader = ssz.electra.ExecutionPayloadHeader.defaultValue(); } diff --git a/packages/light-client/src/spec/utils.ts b/packages/light-client/src/spec/utils.ts index 0c1dccbb7038..027ef87dda48 100644 --- a/packages/light-client/src/spec/utils.ts +++ b/packages/light-client/src/spec/utils.ts @@ -106,8 +106,8 @@ export function upgradeLightClientHeader( // eslint-disable-next-line no-fallthrough case ForkName.electra: - (upgradedHeader as electra.LightClientHeader).execution.depositReceiptsRoot = - ssz.electra.LightClientHeader.fields.execution.fields.depositReceiptsRoot.defaultValue(); + (upgradedHeader as electra.LightClientHeader).execution.depositRequestsRoot = + ssz.electra.LightClientHeader.fields.execution.fields.depositRequestsRoot.defaultValue(); (upgradedHeader as electra.LightClientHeader).execution.withdrawalRequestsRoot = ssz.electra.LightClientHeader.fields.execution.fields.withdrawalRequestsRoot.defaultValue(); @@ -148,7 +148,7 @@ export function isValidLightClientHeader(config: ChainForkConfig, header: allFor if (epoch < config.ELECTRA_FORK_EPOCH) { if ( - (header as electra.LightClientHeader).execution.depositReceiptsRoot !== undefined || + (header as electra.LightClientHeader).execution.depositRequestsRoot !== undefined || (header as electra.LightClientHeader).execution.withdrawalRequestsRoot !== undefined ) { return false; diff --git a/packages/state-transition/src/block/processDeposit.ts b/packages/state-transition/src/block/processDeposit.ts index 676d78e27578..3592fa0b61d1 100644 --- a/packages/state-transition/src/block/processDeposit.ts +++ b/packages/state-transition/src/block/processDeposit.ts @@ -13,7 +13,7 @@ import { } from "@lodestar/params"; import {DepositData} from "@lodestar/types/lib/phase0/types.js"; -import {DepositReceipt} from "@lodestar/types/lib/electra/types.js"; +import {DepositRequest} from "@lodestar/types/lib/electra/types.js"; import {BeaconConfig} from "@lodestar/config"; import {ZERO_HASH} from "../constants/index.js"; import { @@ -54,13 +54,13 @@ export function processDeposit(fork: ForkSeq, state: CachedBeaconStateAllForks, /** * Adds a new validator into the registry. Or increase balance if already exist. - * Follows applyDeposit() in consensus spec. Will be used by processDeposit() and processDepositReceipt() + * Follows applyDeposit() in consensus spec. Will be used by processDeposit() and processDepositRequest() * */ export function applyDeposit( fork: ForkSeq, state: CachedBeaconStateAllForks, - deposit: DepositData | DepositReceipt + deposit: DepositData | DepositRequest ): void { const {config, validators, epochCtx} = state; const {pubkey, withdrawalCredentials, amount} = deposit; diff --git a/packages/state-transition/src/block/processDepositReceipt.ts b/packages/state-transition/src/block/processDepositRequest.ts similarity index 55% rename from packages/state-transition/src/block/processDepositReceipt.ts rename to packages/state-transition/src/block/processDepositRequest.ts index 140e38d634fd..ca6fe4206188 100644 --- a/packages/state-transition/src/block/processDepositReceipt.ts +++ b/packages/state-transition/src/block/processDepositRequest.ts @@ -4,14 +4,14 @@ import {ForkSeq, UNSET_DEPOSIT_RECEIPTS_START_INDEX} from "@lodestar/params"; import {CachedBeaconStateElectra} from "../types.js"; import {applyDeposit} from "./processDeposit.js"; -export function processDepositReceipt( +export function processDepositRequest( fork: ForkSeq, state: CachedBeaconStateElectra, - depositReceipt: electra.DepositReceipt + depositRequest: electra.DepositRequest ): void { - if (state.depositReceiptsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { - state.depositReceiptsStartIndex = BigInt(depositReceipt.index); + if (state.depositRequestsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { + state.depositRequestsStartIndex = BigInt(depositRequest.index); } - applyDeposit(fork, state, depositReceipt); + applyDeposit(fork, state, depositRequest); } diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index 5c8e6086b3c6..67ec549a7715 100644 --- a/packages/state-transition/src/block/processOperations.ts +++ b/packages/state-transition/src/block/processOperations.ts @@ -10,7 +10,7 @@ import {processDeposit} from "./processDeposit.js"; import {processVoluntaryExit} from "./processVoluntaryExit.js"; import {processBlsToExecutionChange} from "./processBlsToExecutionChange.js"; import {processExecutionLayerWithdrawalRequest} from "./processExecutionLayerWithdrawalRequest.js"; -import {processDepositReceipt} from "./processDepositReceipt.js"; +import {processDepositRequest} from "./processDepositRequest.js"; import {ProcessBlockOpts} from "./types.js"; import {processConsolidation} from "./processConsolidation.js"; @@ -22,7 +22,7 @@ export { processVoluntaryExit, processExecutionLayerWithdrawalRequest, processBlsToExecutionChange, - processDepositReceipt, + processDepositRequest, }; export function processOperations( @@ -70,8 +70,8 @@ export function processOperations( processExecutionLayerWithdrawalRequest(fork, state as CachedBeaconStateElectra, elWithdrawalRequest); } - for (const depositReceipt of bodyElectra.executionPayload.depositReceipts) { - processDepositReceipt(fork, stateElectra, depositReceipt); + for (const depositRequest of bodyElectra.executionPayload.depositRequests) { + processDepositRequest(fork, stateElectra, depositRequest); } for (const consolidation of bodyElectra.consolidations) { diff --git a/packages/state-transition/src/slot/upgradeStateToElectra.ts b/packages/state-transition/src/slot/upgradeStateToElectra.ts index 760595c3a86b..a6bc6c331e11 100644 --- a/packages/state-transition/src/slot/upgradeStateToElectra.ts +++ b/packages/state-transition/src/slot/upgradeStateToElectra.ts @@ -25,9 +25,9 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache epoch: stateDeneb.epochCtx.epoch, }); - // latestExecutionPayloadHeader's depositReceiptsRoot and withdrawalRequestsRoot set to zeros by default - // default value of depositReceiptsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX - stateElectra.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + // latestExecutionPayloadHeader's depositRequestsRoot and withdrawalRequestsRoot set to zeros by default + // default value of depositRequestsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX + stateElectra.depositRequestsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; const validatorsArr = stateElectra.validators.getAllReadonly(); diff --git a/packages/state-transition/src/util/deposit.ts b/packages/state-transition/src/util/deposit.ts index 493099fdd982..e8ef93c515d2 100644 --- a/packages/state-transition/src/util/deposit.ts +++ b/packages/state-transition/src/util/deposit.ts @@ -9,9 +9,9 @@ export function getEth1DepositCount(state: CachedBeaconStateAllForks, eth1Data?: // eth1DataIndexLimit = min(UintNum64, UintBn64) can be safely casted as UintNum64 // since the result lies within upper and lower bound of UintNum64 const eth1DataIndexLimit: UintNum64 = - eth1DataToUse.depositCount < electraState.depositReceiptsStartIndex + eth1DataToUse.depositCount < electraState.depositRequestsStartIndex ? eth1DataToUse.depositCount - : Number(electraState.depositReceiptsStartIndex); + : Number(electraState.depositRequestsStartIndex); if (state.eth1DepositIndex < eth1DataIndexLimit) { return Math.min(MAX_DEPOSITS, eth1DataIndexLimit - state.eth1DepositIndex); diff --git a/packages/state-transition/src/util/execution.ts b/packages/state-transition/src/util/execution.ts index 2789836d0e3b..6939c78416b6 100644 --- a/packages/state-transition/src/util/execution.ts +++ b/packages/state-transition/src/util/execution.ts @@ -171,8 +171,8 @@ 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).depositRequestsRoot = + ssz.electra.DepositRequests.hashTreeRoot((payload as electra.ExecutionPayload).depositRequests); (bellatrixPayloadFields as electra.ExecutionPayloadHeader).withdrawalRequestsRoot = ssz.electra.ExecutionLayerWithdrawalRequests.hashTreeRoot( (payload as electra.ExecutionPayload).withdrawalRequests diff --git a/packages/state-transition/src/util/genesis.ts b/packages/state-transition/src/util/genesis.ts index 537edc7f976f..d5f4c577711f 100644 --- a/packages/state-transition/src/util/genesis.ts +++ b/packages/state-transition/src/util/genesis.ts @@ -293,7 +293,7 @@ export function initializeBeaconStateFromEth1( stateElectra.latestExecutionPayloadHeader = (executionPayloadHeader as CompositeViewDU) ?? ssz.electra.ExecutionPayloadHeader.defaultViewDU(); - stateElectra.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + stateElectra.depositRequestsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; } state.commit(); diff --git a/packages/state-transition/test/unit/util/deposit.test.ts b/packages/state-transition/test/unit/util/deposit.test.ts index e8f7f7a86af8..677a15724ece 100644 --- a/packages/state-transition/test/unit/util/deposit.test.ts +++ b/packages/state-transition/test/unit/util/deposit.test.ts @@ -43,7 +43,7 @@ describe("getEth1DepositCount", () => { throw Error("Not a post-Electra state"); } - postElectraState.depositReceiptsStartIndex = 1000n; + postElectraState.depositRequestsStartIndex = 1000n; postElectraState.eth1Data.depositCount = 995; // 1. Should get less than MAX_DEPOSIT @@ -77,7 +77,7 @@ describe("getEth1DepositCount", () => { throw Error("Not a post-Electra state"); } - postElectraState.depositReceiptsStartIndex = 1000n; + postElectraState.depositRequestsStartIndex = 1000n; postElectraState.eth1Data.depositCount = 1005; // Before eth1DepositIndex reaching the start index diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index 26539dd61365..246c731d8382 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -110,7 +110,7 @@ export const SignedAggregateAndProof = new ContainerType( {typeName: "SignedAggregateAndProof", jsonCase: "eth2"} ); -export const DepositReceipt = new ContainerType( +export const DepositRequest = new ContainerType( { pubkey: BLSPubkey, withdrawalCredentials: Bytes32, @@ -118,10 +118,10 @@ export const DepositReceipt = new ContainerType( signature: BLSSignature, index: DepositIndex, }, - {typeName: "DepositReceipt", jsonCase: "eth2"} + {typeName: "DepositRequest", jsonCase: "eth2"} ); -export const DepositReceipts = new ListCompositeType(DepositReceipt, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD); +export const DepositRequests = new ListCompositeType(DepositRequest, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD); export const ExecutionLayerWithdrawalRequest = new ContainerType( { @@ -139,7 +139,7 @@ export const ExecutionLayerWithdrawalRequests = new ListCompositeType( export const ExecutionPayload = new ContainerType( { ...denebSsz.ExecutionPayload.fields, - depositReceipts: DepositReceipts, // New in ELECTRA + depositRequests: DepositRequests, // New in ELECTRA withdrawalRequests: ExecutionLayerWithdrawalRequests, // New in ELECTRA }, {typeName: "ExecutionPayload", jsonCase: "eth2"} @@ -148,7 +148,7 @@ export const ExecutionPayload = new ContainerType( export const ExecutionPayloadHeader = new ContainerType( { ...denebSsz.ExecutionPayloadHeader.fields, - depositReceiptsRoot: Root, // New in ELECTRA + depositRequestsRoot: Root, // New in ELECTRA withdrawalRequestsRoot: Root, // New in ELECTRA }, {typeName: "ExecutionPayloadHeader", jsonCase: "eth2"} @@ -335,7 +335,7 @@ export const BeaconState = new ContainerType( nextWithdrawalValidatorIndex: capellaSsz.BeaconState.fields.nextWithdrawalValidatorIndex, // Deep history valid from Capella onwards historicalSummaries: capellaSsz.BeaconState.fields.historicalSummaries, - depositReceiptsStartIndex: UintBn64, // New in ELECTRA + depositRequestsStartIndex: UintBn64, // New in ELECTRA depositBalanceToConsume: Gwei, // [New in Electra] exitBalanceToConsume: Gwei, // [New in Electra] earliestExitEpoch: Epoch, // [New in Electra] diff --git a/packages/types/src/electra/types.ts b/packages/types/src/electra/types.ts index 4de209b48960..53b95e95525c 100644 --- a/packages/types/src/electra/types.ts +++ b/packages/types/src/electra/types.ts @@ -9,8 +9,8 @@ export type AttesterSlashing = ValueOf; export type AggregateAndProof = ValueOf; export type SignedAggregateAndProof = ValueOf; -export type DepositReceipt = ValueOf; -export type DepositReceipts = ValueOf; +export type DepositRequest = ValueOf; +export type DepositRequests = ValueOf; export type ExecutionLayerWithdrawalRequest = ValueOf; export type ExecutionLayerWithdrawalRequests = ValueOf; From 5c1fb959f2e9a2f64d73c4e226ee8684c158e801 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Wed, 8 May 2024 17:14:44 +0200 Subject: [PATCH 16/51] test: enable spec tests related to eip-7549 (#6741) * initial commit * Update gossip validation * Update attestation gossip validation * aggregateAndProof validation * Extend spec runner to be more flexible * Add missing state attributes for electra * Fix ss data types for electra spec * Make the spec runner more flexible * Fix the bug in process attestation * Update the sepc test version * clean up * Misc * Fix the build erros * feat: get attestations for electra block (#6732) * feat: getAttestationsForBlock() for electra * chore: fix lint * fix: MAX_ATTESTATIONS_PER_GROUP_ELECTRA and address PR comments * chore: unit test aggregateConsolidation * Fix rebase mistake * Address my own comment :) --------- Co-authored-by: Navie Chan * Fix check-types * Address comments * Fix the build erros * Extend spec runner to be more flexible * Add missing state attributes for electra * Fix ss data types for electra spec * Make the spec runner more flexible * Fix the bug in process attestation * Update the sepc test version * Fix rebase issue * Update committee index count check --------- Co-authored-by: NC Co-authored-by: Navie Chan Co-authored-by: tuyennhv --- .../test/spec/presets/operations.test.ts | 4 +-- .../test/spec/specTestVersioning.ts | 2 +- .../test/spec/utils/specTestIterator.ts | 27 +++++++++++++------ .../src/block/processAttestationPhase0.ts | 19 +++++++------ packages/types/src/electra/sszTypes.ts | 20 +++++++------- 5 files changed, 43 insertions(+), 29 deletions(-) diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index 0e6fdafd2256..c2934b55dd62 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -122,8 +122,8 @@ const operations: TestRunnerFn = (fork, sszTypes: { pre: ssz[fork].BeaconState, post: ssz[fork].BeaconState, - attestation: ssz.phase0.Attestation, - attester_slashing: ssz.phase0.AttesterSlashing, + attestation: fork === ForkName.electra ? ssz.electra.Attestation : ssz.phase0.Attestation, + attester_slashing: fork === ForkName.electra ? ssz.electra.AttesterSlashing : ssz.phase0.AttesterSlashing, block: ssz[fork].BeaconBlock, body: ssz[fork].BeaconBlockBody, deposit: ssz.phase0.Deposit, diff --git a/packages/beacon-node/test/spec/specTestVersioning.ts b/packages/beacon-node/test/spec/specTestVersioning.ts index 06b02ab5304e..9195532379a0 100644 --- a/packages/beacon-node/test/spec/specTestVersioning.ts +++ b/packages/beacon-node/test/spec/specTestVersioning.ts @@ -15,7 +15,7 @@ import {DownloadTestsOptions} from "@lodestar/spec-test-util/downloadTests"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export const ethereumConsensusSpecsTests: DownloadTestsOptions = { - specVersion: "v1.5.0-alpha.1", + specVersion: "v1.5.0-alpha.2", // Target directory is the host package root: 'packages/*/spec-tests' outputDir: path.join(__dirname, "../../spec-tests"), specTestsRepoUrl: "https://github.com/ethereum/consensus-spec-tests", diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 88d7cbdea9e6..b99bc281cf50 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -14,7 +14,8 @@ const ARTIFACT_FILENAMES = new Set([ ]); export interface SkipOpts { - skippedPrefixes?: string[]; + skippedTestSuites?: RegExp[]; + skippedTests?: RegExp[]; skippedForks?: string[]; skippedRunners?: string[]; skippedHandlers?: string[]; @@ -57,14 +58,17 @@ const coveredTestRunners = [ // ], // ``` export const defaultSkipOpts: SkipOpts = { - skippedForks: ["electra", "eip7594"], + skippedForks: ["eip7594"], // TODO: capella // BeaconBlockBody proof in lightclient is the new addition in v1.3.0-rc.2-hotfix // Skip them for now to enable subsequently - skippedPrefixes: [ - "capella/light_client/single_merkle_proof/BeaconBlockBody", - "deneb/light_client/single_merkle_proof/BeaconBlockBody", + skippedTestSuites: [ + /^capella\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, + /^deneb\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, + // /^electra\/(?!operations\/attestations)(?!operations\/attester_slashing)/, + /^electra\/(?!operations\/attestation)/, ], + skippedTests: [], skippedRunners: ["merkle_proof", "networking"], }; @@ -100,7 +104,10 @@ export function specTestIterator( opts: SkipOpts = defaultSkipOpts ): void { for (const forkStr of readdirSyncSpec(configDirpath)) { - if (opts?.skippedForks?.includes(forkStr)) { + if ( + opts?.skippedForks?.includes(forkStr) || + (process.env.SPEC_FILTER_FORK && forkStr !== process.env.SPEC_FILTER_FORK) + ) { continue; } const fork = forkStr as ForkName; @@ -134,7 +141,7 @@ export function specTestIterator( for (const testSuite of readdirSyncSpec(testHandlerDirpath)) { const testId = `${fork}/${testRunnerName}/${testHandler}/${testSuite}`; - if (opts?.skippedPrefixes?.some((skippedPrefix) => testId.startsWith(skippedPrefix))) { + if (opts?.skippedTestSuites?.some((skippedMatch) => testId.match(skippedMatch))) { displaySkipTest(testId); } else if (fork === undefined) { displayFailTest(testId, `Unknown fork ${forkStr}`); @@ -150,7 +157,11 @@ export function specTestIterator( // Generic testRunner else { const {testFunction, options} = testRunner.fn(fork, testHandler, testSuite); - + if (opts.skippedTests && options.shouldSkip === undefined) { + options.shouldSkip = (_testCase: any, name: string, _index: number): boolean => { + return opts?.skippedTests?.some((skippedMatch) => name.match(skippedMatch)) ?? false; + }; + } describeDirectorySpecTest(testId, testSuiteDirpath, testFunction, options); } } diff --git a/packages/state-transition/src/block/processAttestationPhase0.ts b/packages/state-transition/src/block/processAttestationPhase0.ts index 9cd1823e4dd4..fb3c9e2da61a 100644 --- a/packages/state-transition/src/block/processAttestationPhase0.ts +++ b/packages/state-transition/src/block/processAttestationPhase0.ts @@ -92,17 +92,20 @@ export function validateAttestation( if (fork >= ForkSeq.electra) { assert.equal(data.index, 0, `AttestationData.index must be zero: index=${data.index}`); const attestationElectra = attestation as electra.Attestation; - const committeeBitsLength = attestationElectra.committeeBits.bitLen; - - if (committeeBitsLength > committeeCount) { - throw new Error( - `Attestation committee bits length are longer than number of committees: committeeBitsLength=${committeeBitsLength} numCommittees=${committeeCount}` - ); - } - // TODO Electra: this should be obsolete soon when the spec switches to committeeIndices const committeeIndices = attestationElectra.committeeBits.getTrueBitIndexes(); + if (committeeIndices.length === 0) { + throw Error("Attestation should have at least one committee bit set"); + } else { + const lastCommitteeIndex = committeeIndices[committeeIndices.length - 1]; + if (lastCommitteeIndex >= committeeCount) { + throw new Error( + `Attestation committee index exceeds committee count: lastCommitteeIndex=${lastCommitteeIndex} numCommittees=${committeeCount}` + ); + } + } + // Get total number of attestation participant of every committee specified const participantCount = committeeIndices .map((committeeIndex) => epochCtx.getBeaconCommittee(data.slot, committeeIndex).length) diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index 246c731d8382..ee8d2702c65d 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -19,8 +19,8 @@ import { MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, MAX_CONSOLIDATIONS, PENDING_BALANCE_DEPOSITS_LIMIT, - PENDING_CONSOLIDATIONS_LIMIT, PENDING_PARTIAL_WITHDRAWALS_LIMIT, + PENDING_CONSOLIDATIONS_LIMIT, } from "@lodestar/params"; import {ssz as primitiveSsz} from "../primitive/index.js"; import {ssz as phase0Ssz} from "../phase0/index.js"; @@ -335,15 +335,15 @@ export const BeaconState = new ContainerType( nextWithdrawalValidatorIndex: capellaSsz.BeaconState.fields.nextWithdrawalValidatorIndex, // Deep history valid from Capella onwards historicalSummaries: capellaSsz.BeaconState.fields.historicalSummaries, - depositRequestsStartIndex: UintBn64, // New in ELECTRA - depositBalanceToConsume: Gwei, // [New in Electra] - exitBalanceToConsume: Gwei, // [New in Electra] - earliestExitEpoch: Epoch, // [New in Electra] - consolidationBalanceToConsume: Gwei, // [New in Electra] - earliestConsolidationEpoch: Epoch, // [New in Electra] - pendingBalanceDeposits: new ListCompositeType(PendingBalanceDeposit, PENDING_BALANCE_DEPOSITS_LIMIT), // [New in Electra] - pendingPartialWithdrawals: new ListCompositeType(PartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT), // [New in Electra] - pendingConsolidations: new ListCompositeType(PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT), // [New in Electra] + depositRequestsStartIndex: UintBn64, // New in ELECTRA:EIP6110 + depositBalanceToConsume: Gwei, // New in Electra:EIP7251 + exitBalanceToConsume: Gwei, // New in Electra:EIP7251 + earliestExitEpoch: Epoch, // New in Electra:EIP7251 + consolidationBalanceToConsume: Gwei, // New in Electra:EIP7251 + earliestConsolidationEpoch: Epoch, // New in Electra:EIP7251 + pendingBalanceDeposits: new ListCompositeType(PendingBalanceDeposit, Number(PENDING_BALANCE_DEPOSITS_LIMIT)), // new in electra:eip7251 + pendingPartialWithdrawals: new ListCompositeType(PartialWithdrawal, Number(PENDING_PARTIAL_WITHDRAWALS_LIMIT)), // New in Electra:EIP7251 + pendingConsolidations: new ListCompositeType(PendingConsolidation, Number(PENDING_CONSOLIDATIONS_LIMIT)), // new in electra:eip7251 }, {typeName: "BeaconState", jsonCase: "eth2"} ); From e7abb892d26ca71b3af4734fcabc0271d40aad3b Mon Sep 17 00:00:00 2001 From: NC Date: Wed, 8 May 2024 18:37:14 +0300 Subject: [PATCH 17/51] fix: fix e2e test in electra-fork (#6751) Update spec version --- packages/params/test/e2e/ensure-config-is-synced.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/params/test/e2e/ensure-config-is-synced.test.ts b/packages/params/test/e2e/ensure-config-is-synced.test.ts index 06fb4bae000c..515d8f44a710 100644 --- a/packages/params/test/e2e/ensure-config-is-synced.test.ts +++ b/packages/params/test/e2e/ensure-config-is-synced.test.ts @@ -8,7 +8,7 @@ import {loadConfigYaml} from "../yaml.js"; // Not e2e, but slow. Run with e2e tests /** https://github.com/ethereum/consensus-specs/releases */ -const specConfigCommit = "v1.4.0-beta.5"; +const specConfigCommit = "v1.5.0-alpha.2"; describe("Ensure config is synced", function () { vi.setConfig({testTimeout: 60 * 1000}); From d4dd8dad422208b76fb502ab261fda2331b6906f Mon Sep 17 00:00:00 2001 From: g11tech Date: Thu, 9 May 2024 00:55:18 +0530 Subject: [PATCH 18/51] feat: get the basic integration working with the ethereumjs electra build (#6752) --- .../el-interop/ethereumjsdocker/electra.tmpl | 94 +++++++++++++++++++ .../test/sim/electra-interop.test.ts | 65 +++++++------ 2 files changed, 126 insertions(+), 33 deletions(-) create mode 100644 packages/beacon-node/test/scripts/el-interop/ethereumjsdocker/electra.tmpl diff --git a/packages/beacon-node/test/scripts/el-interop/ethereumjsdocker/electra.tmpl b/packages/beacon-node/test/scripts/el-interop/ethereumjsdocker/electra.tmpl new file mode 100644 index 000000000000..d557d505c649 --- /dev/null +++ b/packages/beacon-node/test/scripts/el-interop/ethereumjsdocker/electra.tmpl @@ -0,0 +1,94 @@ +{ +"config": { +"chainId":1, +"homesteadBlock":0, +"eip150Block":0, +"eip155Block":0, +"eip158Block":0, +"byzantiumBlock":0, +"constantinopleBlock":0, +"petersburgBlock":0, +"istanbulBlock":0, +"muirGlacierBlock":0, +"berlinBlock":0, +"londonBlock":0, +"shanghaiTime":0, +"cancunTime": 0, +"pragueTime": 0, +"clique": { +"blockperiodseconds": 5, +"epochlength": 30000 +}, +"terminalTotalDifficulty":${TTD} +}, +"nonce":"0x42", +"timestamp":"0x0", +"extraData":"0x0000000000000000000000000000000000000000000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", +"gasLimit":"0x1C9C380", +"difficulty":"0x400000000", +"mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000", +"coinbase":"0x0000000000000000000000000000000000000000", +"alloc":{ + "0x610adc49ecd66cbf176a8247ebd59096c031bd9f": { + "balance": "0x6d6172697573766477000000" + }, + "0xa4664C40AACeBD82A2Db79f0ea36C06Bc6A19Adb": { + "balance": "1000000000000000000000000000" + }, + "0x00000000219ab540356cBB839Cbe05303d7705Fa": { + "balance": "0", + "code": "0x60806040526004361061003f5760003560e01c806301ffc9a71461004457806322895118146100a4578063621fd130146101ba578063c5f2892f14610244575b600080fd5b34801561005057600080fd5b506100906004803603602081101561006757600080fd5b50357fffffffff000000000000000000000000000000000000000000000000000000001661026b565b604080519115158252519081900360200190f35b6101b8600480360360808110156100ba57600080fd5b8101906020810181356401000000008111156100d557600080fd5b8201836020820111156100e757600080fd5b8035906020019184600183028401116401000000008311171561010957600080fd5b91939092909160208101903564010000000081111561012757600080fd5b82018360208201111561013957600080fd5b8035906020019184600183028401116401000000008311171561015b57600080fd5b91939092909160208101903564010000000081111561017957600080fd5b82018360208201111561018b57600080fd5b803590602001918460018302840111640100000000831117156101ad57600080fd5b919350915035610304565b005b3480156101c657600080fd5b506101cf6110b5565b6040805160208082528351818301528351919283929083019185019080838360005b838110156102095781810151838201526020016101f1565b50505050905090810190601f1680156102365780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561025057600080fd5b506102596110c7565b60408051918252519081900360200190f35b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f01ffc9a70000000000000000000000000000000000000000000000000000000014806102fe57507fffffffff0000000000000000000000000000000000000000000000000000000082167f8564090700000000000000000000000000000000000000000000000000000000145b92915050565b6030861461035d576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118056026913960400191505060405180910390fd5b602084146103b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252603681526020018061179c6036913960400191505060405180910390fd5b6060821461040f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260298152602001806118786029913960400191505060405180910390fd5b670de0b6b3a7640000341015610470576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260268152602001806118526026913960400191505060405180910390fd5b633b9aca003406156104cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260338152602001806117d26033913960400191505060405180910390fd5b633b9aca00340467ffffffffffffffff811115610535576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602781526020018061182b6027913960400191505060405180910390fd5b6060610540826114ba565b90507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c589898989858a8a6105756020546114ba565b6040805160a0808252810189905290819060208201908201606083016080840160c085018e8e80828437600083820152601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690910187810386528c815260200190508c8c808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01690920188810386528c5181528c51602091820193918e019250908190849084905b83811015610648578181015183820152602001610630565b50505050905090810190601f1680156106755780820380516001836020036101000a031916815260200191505b5086810383528881526020018989808284376000838201819052601f9091017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169092018881038452895181528951602091820193918b019250908190849084905b838110156106ef5781810151838201526020016106d7565b50505050905090810190601f16801561071c5780820380516001836020036101000a031916815260200191505b509d505050505050505050505050505060405180910390a1600060028a8a600060801b604051602001808484808284377fffffffffffffffffffffffffffffffff0000000000000000000000000000000090941691909301908152604080517ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0818403018152601090920190819052815191955093508392506020850191508083835b602083106107fc57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016107bf565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610859573d6000803e3d6000fd5b5050506040513d602081101561086e57600080fd5b5051905060006002806108846040848a8c6116fe565b6040516020018083838082843780830192505050925050506040516020818303038152906040526040518082805190602001908083835b602083106108f857805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016108bb565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610955573d6000803e3d6000fd5b5050506040513d602081101561096a57600080fd5b5051600261097b896040818d6116fe565b60405160009060200180848480828437919091019283525050604080518083038152602092830191829052805190945090925082918401908083835b602083106109f457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe090920191602091820191016109b7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610a51573d6000803e3d6000fd5b5050506040513d6020811015610a6657600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610ada57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610a9d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610b37573d6000803e3d6000fd5b5050506040513d6020811015610b4c57600080fd5b50516040805160208101858152929350600092600292839287928f928f92018383808284378083019250505093505050506040516020818303038152906040526040518082805190602001908083835b60208310610bd957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610b9c565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610c36573d6000803e3d6000fd5b5050506040513d6020811015610c4b57600080fd5b50516040518651600291889160009188916020918201918291908601908083835b60208310610ca957805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610c6c565b6001836020036101000a0380198251168184511680821785525050505050509050018367ffffffffffffffff191667ffffffffffffffff1916815260180182815260200193505050506040516020818303038152906040526040518082805190602001908083835b60208310610d4e57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610d11565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610dab573d6000803e3d6000fd5b5050506040513d6020811015610dc057600080fd5b5051604080516020818101949094528082019290925280518083038201815260609092019081905281519192909182918401908083835b60208310610e3457805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610df7565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015610e91573d6000803e3d6000fd5b5050506040513d6020811015610ea657600080fd5b50519050858114610f02576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260548152602001806117486054913960600191505060405180910390fd5b60205463ffffffff11610f60576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806117276021913960400191505060405180910390fd5b602080546001019081905560005b60208110156110a9578160011660011415610fa0578260008260208110610f9157fe5b0155506110ac95505050505050565b600260008260208110610faf57fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061102557805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101610fe8565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa158015611082573d6000803e3d6000fd5b5050506040513d602081101561109757600080fd5b50519250600282049150600101610f6e565b50fe5b50505050505050565b60606110c26020546114ba565b905090565b6020546000908190815b60208110156112f05781600116600114156111e6576002600082602081106110f557fe5b01548460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061116b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161112e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156111c8573d6000803e3d6000fd5b5050506040513d60208110156111dd57600080fd5b505192506112e2565b600283602183602081106111f657fe5b015460405160200180838152602001828152602001925050506040516020818303038152906040526040518082805190602001908083835b6020831061126b57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161122e565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa1580156112c8573d6000803e3d6000fd5b5050506040513d60208110156112dd57600080fd5b505192505b6002820491506001016110d1565b506002826112ff6020546114ba565b600060401b6040516020018084815260200183805190602001908083835b6020831061135a57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0909201916020918201910161131d565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790527fffffffffffffffffffffffffffffffffffffffffffffffff000000000000000095909516920191825250604080518083037ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8018152601890920190819052815191955093508392850191508083835b6020831061143f57805182527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09092019160209182019101611402565b51815160209384036101000a7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01801990921691161790526040519190930194509192505080830381855afa15801561149c573d6000803e3d6000fd5b5050506040513d60208110156114b157600080fd5b50519250505090565b60408051600880825281830190925260609160208201818036833701905050905060c082901b8060071a60f81b826000815181106114f457fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060061a60f81b8260018151811061153757fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060051a60f81b8260028151811061157a57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060041a60f81b826003815181106115bd57fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060031a60f81b8260048151811061160057fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060021a60f81b8260058151811061164357fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060011a60f81b8260068151811061168657fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053508060001a60f81b826007815181106116c957fe5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535050919050565b6000808585111561170d578182fd5b83861115611719578182fd5b505082019391909203915056fe4465706f736974436f6e74726163743a206d65726b6c6520747265652066756c6c4465706f736974436f6e74726163743a207265636f6e7374727563746564204465706f7369744461746120646f6573206e6f74206d6174636820737570706c696564206465706f7369745f646174615f726f6f744465706f736974436f6e74726163743a20696e76616c6964207769746864726177616c5f63726564656e7469616c73206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c7565206e6f74206d756c7469706c65206f6620677765694465706f736974436f6e74726163743a20696e76616c6964207075626b6579206c656e6774684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f20686967684465706f736974436f6e74726163743a206465706f7369742076616c756520746f6f206c6f774465706f736974436f6e74726163743a20696e76616c6964207369676e6174757265206c656e677468a2646970667358221220dceca8706b29e917dacf25fceef95acac8d90d765ac926663ce4096195952b6164736f6c634300060b0033", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000022": "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0x0000000000000000000000000000000000000000000000000000000000000023": "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0x0000000000000000000000000000000000000000000000000000000000000024": "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "0x0000000000000000000000000000000000000000000000000000000000000025": "0x536d98837f2dd165a55d5eeae91485954472d56f246df256bf3cae19352a123c", + "0x0000000000000000000000000000000000000000000000000000000000000026": "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", + "0x0000000000000000000000000000000000000000000000000000000000000027": "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0x0000000000000000000000000000000000000000000000000000000000000028": "0x87eb0ddba57e35f6d286673802a4af5975e22506c7cf4c64bb6be5ee11527f2c", + "0x0000000000000000000000000000000000000000000000000000000000000029": "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x000000000000000000000000000000000000000000000000000000000000002a": "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0x000000000000000000000000000000000000000000000000000000000000002b": "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x000000000000000000000000000000000000000000000000000000000000002c": "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0x000000000000000000000000000000000000000000000000000000000000002d": "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0x000000000000000000000000000000000000000000000000000000000000002e": "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", + "0x000000000000000000000000000000000000000000000000000000000000002f": "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", + "0x0000000000000000000000000000000000000000000000000000000000000030": "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "0x0000000000000000000000000000000000000000000000000000000000000031": "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", + "0x0000000000000000000000000000000000000000000000000000000000000032": "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", + "0x0000000000000000000000000000000000000000000000000000000000000033": "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0x0000000000000000000000000000000000000000000000000000000000000034": "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0x0000000000000000000000000000000000000000000000000000000000000035": "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x0000000000000000000000000000000000000000000000000000000000000036": "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0x0000000000000000000000000000000000000000000000000000000000000037": "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0x0000000000000000000000000000000000000000000000000000000000000038": "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x0000000000000000000000000000000000000000000000000000000000000039": "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x000000000000000000000000000000000000000000000000000000000000003a": "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x000000000000000000000000000000000000000000000000000000000000003b": "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x000000000000000000000000000000000000000000000000000000000000003c": "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x000000000000000000000000000000000000000000000000000000000000003d": "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x000000000000000000000000000000000000000000000000000000000000003e": "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0x000000000000000000000000000000000000000000000000000000000000003f": "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x0000000000000000000000000000000000000000000000000000000000000040": "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7" + } + }, + "0x25a219378dad9b3503c8268c9ca836a52427a4fb": { + "balance": "0", + "nonce": "1", + "code": "0x60203611603157600143035f35116029575f356120000143116029576120005f3506545f5260205ff35b5f5f5260205ff35b5f5ffd00" + }, + "0x00A3ca265EBcb825B45F985A16CEFB49958cE017": { + "balance": "0", + "nonce": "1", + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe146090573615156028575f545f5260205ff35b366038141561012e5760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565b90939004341061012e57600154600101600155600354806003026004013381556001015f3581556001016020359055600101600355005b6003546002548082038060101160a4575060105b5f5b81811460dd5780604c02838201600302600401805490600101805490600101549160601b83528260140152906034015260010160a6565b910180921460ed579060025560f8565b90505f6002555f6003555b5f548061049d141561010757505f5b60015460028282011161011c5750505f610122565b01600290035b5f555f600155604c025ff35b5f5ffd", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": + "0x000000000000000000000000000000000000000000000000000000000000049d" + } + } +}, +"number":"0x0", +"gasUsed":"0x0", +"parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", +"baseFeePerGas":"0x7" +} \ No newline at end of file diff --git a/packages/beacon-node/test/sim/electra-interop.test.ts b/packages/beacon-node/test/sim/electra-interop.test.ts index c503052a74f4..78010ff364ad 100644 --- a/packages/beacon-node/test/sim/electra-interop.test.ts +++ b/packages/beacon-node/test/sim/electra-interop.test.ts @@ -28,9 +28,7 @@ import {logFilesDir} from "./params.js"; import {shell} from "./shell.js"; // NOTE: How to run -// DEV_RUN=true EL_BINARY_DIR=naviechan/besu:v6110 EL_SCRIPT_DIR=besudocker yarn vitest --run test/sim/electra-interop.test.ts -// or -// DEV_RUN=true EL_BINARY_DIR=/Volumes/fast_boi/navie/Documents/workspace/besu/build/install/besu/bin EL_SCRIPT_DIR=besu yarn vitest --run test/sim/electra-interop.test.ts +// DEV_RUN=true EL_BINARY_DIR=ethpandaops/ethereumjs:master-0e06ddf EL_SCRIPT_DIR=ethereumjsdocker yarn vitest --run test/sim/electra-interop.test.ts // ``` /* eslint-disable no-console, @typescript-eslint/naming-convention */ @@ -110,35 +108,35 @@ describe("executionEngine / ExecutionEngineHttp", function () { // 2. Send raw deposit transaction A and B. tx A is to be imported via newPayload, tx B is to be included in payload via getPayload const depositTransactionA = - "0x02f9021c8217de808459682f008459682f0e830271009442424242424242424242424242424242424242428901bc16d674ec800000b901a422895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120749715de5d1226545c6b3790f515d551a5cc5bf1d49c87a696860554d2fc4f14000000000000000000000000000000000000000000000000000000000000003096a96086cff07df17668f35f7418ef8798079167e3f4f9b72ecde17b28226137cf454ab1dd20ef5d924786ab3483c2f9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020003f5102dabe0a27b1746098d1dc17a5d3fbd478759fea9287e4e419b3c3cef20000000000000000000000000000000000000000000000000000000000000060b1acdb2c4d3df3f1b8d3bfd33421660df358d84d78d16c4603551935f4b67643373e7eb63dcb16ec359be0ec41fee33b03a16e80745f2374ff1d3c352508ac5d857c6476d3c3bcf7e6ca37427c9209f17be3af5264c0e2132b3dd1156c28b4e9c080a09f597089338d7f44f5c59f8230bb38f243849228a8d4e9d2e2956e6050f5b2c7a076486996c7e62802b8f95eee114783e4b403fd11093ba96286ff42c595f24452"; + "0x02f90213018080648401c9c3809400000000219ab540356cbb839cbe05303d7705fa8901bc16d674ec800000b901a422895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001208cd4e5a69709cf8ee5b1b73d6efbf3f33bcac92fb7e4ce62b2467542fb50a72d0000000000000000000000000000000000000000000000000000000000000030ac842878bb70009552a4cfcad801d6e659c50bd50d7d03306790cb455ce7363c5b6972f0159d170f625a99b2064dbefc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020010000000000000000000000818ccb1c4eda80270b04d6df822b1e72dd83c3030000000000000000000000000000000000000000000000000000000000000060a747f75c72d0cf0d2b52504c7385b516f0523e2f0842416399f42b4aee5c6384a5674f6426b1cc3d0827886fa9b909e616f5c9f61f986013ed2b9bf37071cbae951136265b549f44e3c8e26233c0433e9124b7fd0dc86e82f9fedfc0a179d769c080a067c9857d27a42f8fde4d5cf2d6c324af94469ac93ec867eacdd9002e1297835fa07927224866e03d51fb1ae94390e7aec453cad8df9e048892e98f945178eab254"; const depositRequestA = { amount: 32000000000, index: 0, pubkey: dataToBytes( - "0x96a96086cff07df17668f35f7418ef8798079167e3f4f9b72ecde17b28226137cf454ab1dd20ef5d924786ab3483c2f9", + "0xac842878bb70009552a4cfcad801d6e659c50bd50d7d03306790cb455ce7363c5b6972f0159d170f625a99b2064dbefc", 48 ), signature: dataToBytes( - "0xb1acdb2c4d3df3f1b8d3bfd33421660df358d84d78d16c4603551935f4b67643373e7eb63dcb16ec359be0ec41fee33b03a16e80745f2374ff1d3c352508ac5d857c6476d3c3bcf7e6ca37427c9209f17be3af5264c0e2132b3dd1156c28b4e9", + "0xa747f75c72d0cf0d2b52504c7385b516f0523e2f0842416399f42b4aee5c6384a5674f6426b1cc3d0827886fa9b909e616f5c9f61f986013ed2b9bf37071cbae951136265b549f44e3c8e26233c0433e9124b7fd0dc86e82f9fedfc0a179d769", 96 ), - withdrawalCredentials: dataToBytes("0x003f5102dabe0a27b1746098d1dc17a5d3fbd478759fea9287e4e419b3c3cef2", 32), + withdrawalCredentials: dataToBytes("0x010000000000000000000000818ccb1c4eda80270b04d6df822b1e72dd83c303", 32), }; const depositTransactionB = - "0x02f9021c8217de018459682f008459682f0e830271009442424242424242424242424242424242424242428901bc16d674ec800000b901a422895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120a18b4c7cab0afa273ea9504904521ea8421a4e32740b7611bd3d5095ca99f0cb0000000000000000000000000000000000000000000000000000000000000030a5c85a60ba2905c215f6a12872e62b1ee037051364244043a5f639aa81b04a204c55e7cc851f29c7c183be253ea1510b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020001db70c485b6264692f26b8aeaab5b0c384180df8e2184a21a808a3ec8e86ca00000000000000000000000000000000000000000000000000000000000000609561731785b48cf1886412234531e4940064584463e96ac63a1a154320227e333fb51addc4a89b7e0d3f862d7c1fd4ea03bd8eb3d8806f1e7daf591cbbbb92b0beb74d13c01617f22c5026b4f9f9f294a8a7c32db895de3b01bee0132c9209e1c001a0644e0a763a34b4bfb9f56a677857b57fcf15e3db57e2f57060e92084f75f3d82a018ba8eaacbd8e6f6917675b1d0362b12ca82850ca8ef9c010430760c2b2e0cb5"; + "0x02f90213010180648401c9c3809400000000219ab540356cbb839cbe05303d7705fa8901bc16d674ec800000b901a422895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120a7ec6a3459bf9389265f62abbdffcd0ef20924bd03e4856d3b964edf565bd8e80000000000000000000000000000000000000000000000000000000000000030a5290ddb9abd6a7fb8bac3414c6c7ff093a18ff297c1eada20464de388b14aafa505bfc98847ca7e6f7ca3aa9d4ca769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020010000000000000000000000da628fed218cbe3a9e684a9f51c49dd63a229a1d000000000000000000000000000000000000000000000000000000000000006080e12262f94795ce3453f17eea2dd44843ff7977d303b192c1d2a4ce0dbebc8856c398d6445cbf244ba9e99307ead1e30b2544a5e9693cdd5196a33c46e2dd8a8b83afc8278c1ea79cd5c13cac2b96a62257b3636787d0f1e0f881c50a4667ddc080a0b653aad27e504d4fcd19b7c317ffbd2a26a81d6ac14ecea6a891a63dcf7816dfa02953273b4cddc93b2a9ba21aaeb0db988cb1086319dd0b91f79bc101adfe32e4"; const depositRequestB = { amount: 32000000000, index: 1, pubkey: dataToBytes( - "0xa5c85a60ba2905c215f6a12872e62b1ee037051364244043a5f639aa81b04a204c55e7cc851f29c7c183be253ea1510b", + "0xa5290ddb9abd6a7fb8bac3414c6c7ff093a18ff297c1eada20464de388b14aafa505bfc98847ca7e6f7ca3aa9d4ca769", 48 ), signature: dataToBytes( - "0x9561731785b48cf1886412234531e4940064584463e96ac63a1a154320227e333fb51addc4a89b7e0d3f862d7c1fd4ea03bd8eb3d8806f1e7daf591cbbbb92b0beb74d13c01617f22c5026b4f9f9f294a8a7c32db895de3b01bee0132c9209e1", + "0x80e12262f94795ce3453f17eea2dd44843ff7977d303b192c1d2a4ce0dbebc8856c398d6445cbf244ba9e99307ead1e30b2544a5e9693cdd5196a33c46e2dd8a8b83afc8278c1ea79cd5c13cac2b96a62257b3636787d0f1e0f881c50a4667dd", 96 ), - withdrawalCredentials: dataToBytes("0x001db70c485b6264692f26b8aeaab5b0c384180df8e2184a21a808a3ec8e86ca", 32), + withdrawalCredentials: dataToBytes("0x010000000000000000000000da628fed218cbe3a9e684a9f51c49dd63a229a1d", 32), }; sendRawTransactionBig(ethRpcUrl, depositTransactionA, `${dataPath}/deposit.json`).catch((e: Error) => { @@ -150,18 +148,18 @@ describe("executionEngine / ExecutionEngineHttp", function () { }); // 3. Import new payload with tx A and deposit receipt A - const newPayloadBlockHash = "0xfd1189e6ea0814b7d40d4e50b31ae5feabbb2acff39399457bbdda7cb5ccd490"; + const newPayloadBlockHash = "0x4cec1852552239cf78e8bd2db35ff9396acb6b40c3ce486e6e3028bc75c9faec"; const newPayload = { - parentHash: dataToBytes("0x26118cf71453320edcebbc4ebb34af5b578087a32385b80108bf691fa23efc42", 32), + parentHash: dataToBytes("0xeb86e5aca89ea5477a6e169a389efbbe7e5a3d5f5c5296bcde3a4b032ea9bae8", 32), feeRecipient: dataToBytes("0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", 20), - stateRoot: dataToBytes("0x14208ac0e218167936e220b72d5d5887a963cb858ea2f2d268518f014a3da3fa", 32), + stateRoot: dataToBytes("0x686ce0478cabce79b298712fefee4aefd2fac1ab4a4813936d2c1ccca788bbc3", 32), logsBloom: dataToBytes( - "0x10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000", + "0x00000000000000000000400000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000020000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000", 256 ), prevRandao: dataToBytes("0x0000000000000000000000000000000000000000000000000000000000000000", 32), gasLimit: 30000000, - gasUsed: 84846, + gasUsed: 84714, timestamp: 16, extraData: dataToBytes("0x", 0), baseFeePerGas: 7n, @@ -171,7 +169,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { depositRequests: [depositRequestA], blockNumber: 1, blockHash: dataToBytes(newPayloadBlockHash, 32), - receiptsRoot: dataToBytes("0x79ee3424eb720a3ad4b1c5a372bb8160580cbe4d893778660f34213c685627a9", 32), + receiptsRoot: dataToBytes("0x0b67bea29f17eeb290685e01e9a2e4cd77a83471d9985a8ce27997a7ed3ee3f8", 32), blobGasUsed: 0n, withdrawalRequests: [], }; @@ -235,21 +233,22 @@ describe("executionEngine / ExecutionEngineHttp", function () { ); }); - it("Post-merge, run for a few blocks", async function () { - console.log("\n\nPost-merge, run for a few blocks\n\n"); - const {elClient, tearDownCallBack} = await runEL( - {...elSetupConfig, mode: ELStartMode.PostMerge, genesisTemplate: "electra.tmpl"}, - {...elRunOptions, ttd: BigInt(0)}, - controller.signal - ); - afterEachCallbacks.push(() => tearDownCallBack()); - - await runNodeWithEL({ - elClient, - electraEpoch: 0, - testName: "post-merge", - }); - }); + // TODO: get this post merge run working + // it("Post-merge, run for a few blocks", async function () { + // console.log("\n\nPost-merge, run for a few blocks\n\n"); + // const {elClient, tearDownCallBack} = await runEL( + // {...elSetupConfig, mode: ELStartMode.PostMerge, genesisTemplate: "electra.tmpl"}, + // {...elRunOptions, ttd: BigInt(0)}, + // controller.signal + // ); + // afterEachCallbacks.push(() => tearDownCallBack()); + + // await runNodeWithEL({ + // elClient, + // electraEpoch: 0, + // testName: "post-merge", + // }); + // }); /** * Want to test two things: @@ -364,7 +363,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { // send raw tx at slot 1 const depositTransaction = - "0x02f9021e8217de8085012a05f20085019254d380830271009442424242424242424242424242424242424242428901bc16d674ec800000b901a422895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120ef950826b191ebea0bbafa92a2c6bffa8239c6f456d92891ce2852b8360f0d30000000000000000000000000000000000000000000000000000000000000003095e4f91aea91a9e00387fad9d60997cff6cbf68d42d1b6629a7b248cdef255f94a2a2381e5d4125273fe42da5f7aa0e1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020003f5102dabe0a27b1746098d1dc17a5d3fbd478759fea9287e4e419b3c3cef20000000000000000000000000000000000000000000000000000000000000060b6c06e65228046268aa918baf78e072c25e65aa0bcf258cefcac3371c47df81bc4d43ca942f5fc28f9a563e925fd9c5010bc8c300add3faf3af0d61fabaaf03694020feaafb03e47c1bc4fcf082684c7ed3f7d5839d1722214b24f95ad2b226cc080a0be1161617492e4ca2fcb89edcadf5e71e8cac0d6447d18cfde9b55e5a8412417a07ec8c47dd484036c745049bb2e2980d44e38d4dacac50dc4a14a2f23c52f2e5f"; + "0x02f90213018080648401c9c3809400000000219ab540356cbb839cbe05303d7705fa8901bc16d674ec800000b901a422895118000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000001208cd4e5a69709cf8ee5b1b73d6efbf3f33bcac92fb7e4ce62b2467542fb50a72d0000000000000000000000000000000000000000000000000000000000000030ac842878bb70009552a4cfcad801d6e659c50bd50d7d03306790cb455ce7363c5b6972f0159d170f625a99b2064dbefc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020010000000000000000000000818ccb1c4eda80270b04d6df822b1e72dd83c3030000000000000000000000000000000000000000000000000000000000000060a747f75c72d0cf0d2b52504c7385b516f0523e2f0842416399f42b4aee5c6384a5674f6426b1cc3d0827886fa9b909e616f5c9f61f986013ed2b9bf37071cbae951136265b549f44e3c8e26233c0433e9124b7fd0dc86e82f9fedfc0a179d769c080a067c9857d27a42f8fde4d5cf2d6c324af94469ac93ec867eacdd9002e1297835fa07927224866e03d51fb1ae94390e7aec453cad8df9e048892e98f945178eab254"; sendRawTransactionBig(ethRpcUrl, depositTransaction, `${dataPath}/deposit.json`).catch((e: Error) => { loggerNodeA.error("Fail to send raw deposit transaction", undefined, e); }); From aed7d7f41cd48c47e61f258f17c80094c794a14c Mon Sep 17 00:00:00 2001 From: g11tech Date: Thu, 9 May 2024 22:29:39 +0530 Subject: [PATCH 19/51] feat: apply some fixes and hacks to get the single node devnet working with fork transition (#6754) --- .../chain/produceBlock/produceBlockBody.ts | 5 ++ .../src/slot/upgradeStateToElectra.ts | 74 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 988e3ca135e1..a511fa331687 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -11,6 +11,7 @@ import { capella, deneb, Wei, + electra, } from "@lodestar/types"; import { CachedBeaconStateAllForks, @@ -339,6 +340,10 @@ export async function produceBlockBody( } } + if (ForkSeq[fork] >= ForkSeq.electra) { + (blockBody as electra.BeaconBlockBody).consolidations = []; + } + Object.assign(logMeta, {executionPayloadValue}); this.logger.verbose("Produced beacon block body", logMeta); diff --git a/packages/state-transition/src/slot/upgradeStateToElectra.ts b/packages/state-transition/src/slot/upgradeStateToElectra.ts index a6bc6c331e11..121425265b50 100644 --- a/packages/state-transition/src/slot/upgradeStateToElectra.ts +++ b/packages/state-transition/src/slot/upgradeStateToElectra.ts @@ -14,6 +14,80 @@ import { export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): CachedBeaconStateElectra { const {config} = stateDeneb; + ssz.deneb.BeaconState.commitViewDU(stateDeneb); + const stateElectraCloned = stateDeneb; + + const stateElectraView = ssz.electra.BeaconState.defaultViewDU(); + stateElectraView.genesisTime = stateElectraCloned.genesisTime; + stateElectraView.genesisValidatorsRoot = stateElectraCloned.genesisValidatorsRoot; + stateElectraView.slot = stateElectraCloned.slot; + stateElectraView.fork = ssz.phase0.Fork.toViewDU({ + previousVersion: stateDeneb.fork.currentVersion, + currentVersion: config.ELECTRA_FORK_VERSION, + epoch: stateDeneb.epochCtx.epoch, + }); + stateElectraView.latestBlockHeader = stateElectraCloned.latestBlockHeader; + stateElectraView.blockRoots = stateElectraCloned.blockRoots; + stateElectraView.stateRoots = stateElectraCloned.stateRoots; + stateElectraView.historicalRoots = stateElectraCloned.historicalRoots; + stateElectraView.eth1Data = stateElectraCloned.eth1Data; + stateElectraView.eth1DataVotes = stateElectraCloned.eth1DataVotes; + stateElectraView.eth1DepositIndex = stateElectraCloned.eth1DepositIndex; + stateElectraView.validators = stateElectraCloned.validators; + stateElectraView.balances = stateElectraCloned.balances; + stateElectraView.randaoMixes = stateElectraCloned.randaoMixes; + stateElectraView.slashings = stateElectraCloned.slashings; + stateElectraView.previousEpochParticipation = stateElectraCloned.previousEpochParticipation; + stateElectraView.currentEpochParticipation = stateElectraCloned.currentEpochParticipation; + stateElectraView.justificationBits = stateElectraCloned.justificationBits; + stateElectraView.previousJustifiedCheckpoint = stateElectraCloned.previousJustifiedCheckpoint; + stateElectraView.currentJustifiedCheckpoint = stateElectraCloned.currentJustifiedCheckpoint; + stateElectraView.finalizedCheckpoint = stateElectraCloned.finalizedCheckpoint; + stateElectraView.inactivityScores = stateElectraCloned.inactivityScores; + stateElectraView.currentSyncCommittee = stateElectraCloned.currentSyncCommittee; + stateElectraView.nextSyncCommittee = stateElectraCloned.nextSyncCommittee; + stateElectraView.latestExecutionPayloadHeader = ssz.electra.BeaconState.fields.latestExecutionPayloadHeader.toViewDU({ + ...stateElectraCloned.latestExecutionPayloadHeader.toValue(), + depositRequestsRoot: ssz.Root.defaultValue(), + withdrawalRequestsRoot: ssz.Root.defaultValue(), + }); + stateElectraView.nextWithdrawalIndex = stateDeneb.nextWithdrawalIndex; + stateElectraView.nextWithdrawalValidatorIndex = stateDeneb.nextWithdrawalValidatorIndex; + stateElectraView.historicalSummaries = stateElectraCloned.historicalSummaries; + + // latestExecutionPayloadHeader's depositRequestsRoot and withdrawalRequestsRoot set to zeros by default + // default value of depositRequestsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX + stateElectraView.depositRequestsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + + const validatorsArr = stateElectraView.validators.getAllReadonly(); + + for (let i = 0; i < validatorsArr.length; i++) { + const validator = validatorsArr[i]; + + // [EIP-7251]: add validators that are not yet active to pending balance deposits + if (validator.activationEligibilityEpoch === FAR_FUTURE_EPOCH) { + queueEntireBalanceAndResetValidator(stateElectraView as CachedBeaconStateElectra, i); + } + + // [EIP-7251]: Ensure early adopters of compounding credentials go through the activation churn + const withdrawalCredential = validator.withdrawalCredentials; + if (hasCompoundingWithdrawalCredential(withdrawalCredential)) { + queueExcessActiveBalance(stateElectraView as CachedBeaconStateElectra, i); + } + } + + const stateElectra = getCachedBeaconState(stateElectraView, stateDeneb); + // Commit new added fields ViewDU to the root node + stateElectra.commit(); + // Clear cache to ensure the cache of deneb fields is not used by new ELECTRA fields + stateElectra["clearCache"](); + + return stateElectra; +} + +export function upgradeStateToElectraOriginal(stateDeneb: CachedBeaconStateDeneb): CachedBeaconStateElectra { + const {config} = stateDeneb; + const stateElectraNode = ssz.deneb.BeaconState.commitViewDU(stateDeneb); const stateElectraView = ssz.electra.BeaconState.getViewDU(stateElectraNode); From 14695b5c46ed81c45faeefe6421187b9907ffb7a Mon Sep 17 00:00:00 2001 From: twoeths Date: Fri, 10 May 2024 17:52:08 +0700 Subject: [PATCH 20/51] fix: get aggregate and proofs signature sets (#6757) fix: get signature for SignedAggregateAndProof based on fork --- .../validation/signatureSets/aggregateAndProof.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts index 2bc2e62c861f..1c18723feac0 100644 --- a/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts @@ -1,6 +1,6 @@ import type {PublicKey} from "@chainsafe/bls/types"; -import {DOMAIN_AGGREGATE_AND_PROOF} from "@lodestar/params"; -import {ssz} from "@lodestar/types"; +import {DOMAIN_AGGREGATE_AND_PROOF, ForkSeq} from "@lodestar/params"; +import {allForks, ssz} from "@lodestar/types"; import {Epoch, phase0} from "@lodestar/types"; import { computeSigningRoot, @@ -13,7 +13,7 @@ import {BeaconConfig} from "@lodestar/config"; export function getAggregateAndProofSigningRoot( config: BeaconConfig, epoch: Epoch, - aggregateAndProof: phase0.SignedAggregateAndProof + aggregateAndProof: allForks.SignedAggregateAndProof ): Uint8Array { // previously, we call `const aggregatorDomain = state.config.getDomain(state.slot, DOMAIN_AGGREGATE_AND_PROOF, slot);` // at fork boundary, it's required to dial to target epoch https://github.com/ChainSafe/lodestar/blob/v1.11.3/packages/beacon-node/src/chain/validation/attestation.ts#L573 @@ -21,14 +21,15 @@ export function getAggregateAndProofSigningRoot( const slot = computeStartSlotAtEpoch(epoch); const fork = config.getForkName(slot); const aggregatorDomain = config.getDomainAtFork(fork, DOMAIN_AGGREGATE_AND_PROOF); - return computeSigningRoot(ssz.phase0.AggregateAndProof, aggregateAndProof.message, aggregatorDomain); + const sszType = ForkSeq[fork] >= ForkSeq.electra ? ssz.electra.AggregateAndProof : ssz.phase0.AggregateAndProof; + return computeSigningRoot(sszType, aggregateAndProof.message, aggregatorDomain); } export function getAggregateAndProofSignatureSet( config: BeaconConfig, epoch: Epoch, aggregator: PublicKey, - aggregateAndProof: phase0.SignedAggregateAndProof + aggregateAndProof: allForks.SignedAggregateAndProof ): ISignatureSet { return createSingleSignatureSetFromComponents( aggregator, From 4a552112207c34fe7a9cb1aa597a32baedd1d9cc Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Sat, 11 May 2024 00:01:42 +0200 Subject: [PATCH 21/51] test(spec): fix attestors slashing specs for electra fork (#6758) * Fix attester slashing specs for electra * Remove unused import * Add code comment * Update the expression * Update the fork check --- packages/beacon-node/test/spec/utils/specTestIterator.ts | 3 +-- .../src/block/isValidIndexedAttestation.ts | 8 ++++++-- packages/state-transition/src/util/epoch.ts | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index b99bc281cf50..393983bb8a56 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -65,8 +65,7 @@ export const defaultSkipOpts: SkipOpts = { skippedTestSuites: [ /^capella\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, /^deneb\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, - // /^electra\/(?!operations\/attestations)(?!operations\/attester_slashing)/, - /^electra\/(?!operations\/attestation)/, + /^electra\/(?!operations\/attestations)(?!operations\/attester_slashing)/, ], skippedTests: [], skippedRunners: ["merkle_proof", "networking"], diff --git a/packages/state-transition/src/block/isValidIndexedAttestation.ts b/packages/state-transition/src/block/isValidIndexedAttestation.ts index e3965b97ee73..33d92a208260 100644 --- a/packages/state-transition/src/block/isValidIndexedAttestation.ts +++ b/packages/state-transition/src/block/isValidIndexedAttestation.ts @@ -1,4 +1,4 @@ -import {MAX_VALIDATORS_PER_COMMITTEE} from "@lodestar/params"; +import {ForkSeq, MAX_COMMITTEES_PER_SLOT, MAX_VALIDATORS_PER_COMMITTEE} from "@lodestar/params"; import {phase0} from "@lodestar/types"; import {CachedBeaconStateAllForks} from "../types.js"; import {verifySignatureSet} from "../util/index.js"; @@ -44,7 +44,11 @@ export function isValidIndexedAttestationBigint( */ export function isValidIndexedAttestationIndices(state: CachedBeaconStateAllForks, indices: number[]): boolean { // verify max number of indices - if (!(indices.length > 0 && indices.length <= MAX_VALIDATORS_PER_COMMITTEE)) { + const maxIndices = + state.config.getForkSeq(state.slot) >= ForkSeq.electra + ? MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT + : MAX_VALIDATORS_PER_COMMITTEE; + if (!(indices.length > 0 && indices.length <= maxIndices)) { return false; } diff --git a/packages/state-transition/src/util/epoch.ts b/packages/state-transition/src/util/epoch.ts index e399d3276e3e..ca56b711e0be 100644 --- a/packages/state-transition/src/util/epoch.ts +++ b/packages/state-transition/src/util/epoch.ts @@ -52,7 +52,7 @@ export function computeExitEpochAndUpdateChurn(state: CachedBeaconStateElectra, // Exit doesn't fit in the current earliest epoch. if (exitBalance > exitBalanceToConsume) { const balanceToProcess = Number(exitBalance) - exitBalanceToConsume; - const additionalEpochs = Math.floor((balanceToProcess - 1) / (perEpochChurn + 1)); + const additionalEpochs = Math.floor((balanceToProcess - 1) / perEpochChurn) + 1; earliestExitEpoch += additionalEpochs; exitBalanceToConsume += additionalEpochs * perEpochChurn; } From fda27b8c07eec1df8162438ad9a96f3b5663ce51 Mon Sep 17 00:00:00 2001 From: g11tech Date: Mon, 13 May 2024 14:02:31 +0530 Subject: [PATCH 22/51] chore: fix types and lint (#6750) * chore: fix types and lint * fx * type and lint fix --- .../src/api/impl/config/constants.ts | 10 +++++++ .../test/sim/electra-interop.test.ts | 30 +++++++++---------- .../spec/presets/epoch_processing.test.ts | 10 +++++-- .../test/spec/presets/operations.test.ts | 3 +- packages/types/src/electra/index.ts | 5 ++-- 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/packages/beacon-node/src/api/impl/config/constants.ts b/packages/beacon-node/src/api/impl/config/constants.ts index 87ffce91b4d9..b5f810157581 100644 --- a/packages/beacon-node/src/api/impl/config/constants.ts +++ b/packages/beacon-node/src/api/impl/config/constants.ts @@ -36,6 +36,10 @@ import { SYNC_COMMITTEE_SUBNET_COUNT, BLOB_TX_TYPE, VERSIONED_HASH_VERSION_KZG, + COMPOUNDING_WITHDRAWAL_PREFIX, + DOMAIN_CONSOLIDATION, + UNSET_DEPOSIT_RECEIPTS_START_INDEX, + FULL_EXIT_REQUEST_AMOUNT, } from "@lodestar/params"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -57,6 +61,7 @@ export const specConstants = { // ## Withdrawal prefixes BLS_WITHDRAWAL_PREFIX, ETH1_ADDRESS_WITHDRAWAL_PREFIX, + COMPOUNDING_WITHDRAWAL_PREFIX, // ## Domain types DOMAIN_BEACON_PROPOSER, DOMAIN_BEACON_ATTESTER, @@ -66,6 +71,7 @@ export const specConstants = { DOMAIN_SELECTION_PROOF, DOMAIN_AGGREGATE_AND_PROOF, DOMAIN_APPLICATION_BUILDER, + DOMAIN_CONSOLIDATION, // phase0/validator.md TARGET_AGGREGATORS_PER_COMMITTEE, @@ -100,4 +106,8 @@ export const specConstants = { // Deneb types BLOB_TX_TYPE, VERSIONED_HASH_VERSION_KZG, + + // electra + UNSET_DEPOSIT_RECEIPTS_START_INDEX, + FULL_EXIT_REQUEST_AMOUNT, }; diff --git a/packages/beacon-node/test/sim/electra-interop.test.ts b/packages/beacon-node/test/sim/electra-interop.test.ts index 78010ff364ad..2dfa5ee0f77c 100644 --- a/packages/beacon-node/test/sim/electra-interop.test.ts +++ b/packages/beacon-node/test/sim/electra-interop.test.ts @@ -234,21 +234,21 @@ describe("executionEngine / ExecutionEngineHttp", function () { }); // TODO: get this post merge run working - // it("Post-merge, run for a few blocks", async function () { - // console.log("\n\nPost-merge, run for a few blocks\n\n"); - // const {elClient, tearDownCallBack} = await runEL( - // {...elSetupConfig, mode: ELStartMode.PostMerge, genesisTemplate: "electra.tmpl"}, - // {...elRunOptions, ttd: BigInt(0)}, - // controller.signal - // ); - // afterEachCallbacks.push(() => tearDownCallBack()); - - // await runNodeWithEL({ - // elClient, - // electraEpoch: 0, - // testName: "post-merge", - // }); - // }); + it.skip("Post-merge, run for a few blocks", async function () { + console.log("\n\nPost-merge, run for a few blocks\n\n"); + const {elClient, tearDownCallBack} = await runEL( + {...elSetupConfig, mode: ELStartMode.PostMerge, genesisTemplate: "electra.tmpl"}, + {...elRunOptions, ttd: BigInt(0)}, + controller.signal + ); + afterEachCallbacks.push(() => tearDownCallBack()); + + await runNodeWithEL({ + elClient, + electraEpoch: 0, + testName: "post-merge", + }); + }); /** * Want to test two things: diff --git a/packages/beacon-node/test/spec/presets/epoch_processing.test.ts b/packages/beacon-node/test/spec/presets/epoch_processing.test.ts index a244762143f3..b089863fdad3 100644 --- a/packages/beacon-node/test/spec/presets/epoch_processing.test.ts +++ b/packages/beacon-node/test/spec/presets/epoch_processing.test.ts @@ -22,7 +22,10 @@ export type EpochTransitionFn = (state: CachedBeaconStateAllForks, epochTransiti /* eslint-disable @typescript-eslint/naming-convention */ const epochTransitionFns: Record = { - effective_balance_updates: epochFns.processEffectiveBalanceUpdates, + effective_balance_updates: (state, epochTransitionCache) => { + const fork = state.config.getForkSeq(state.slot); + epochFns.processEffectiveBalanceUpdates(fork, state, epochTransitionCache); + }, eth1_data_reset: epochFns.processEth1DataReset, historical_roots_update: epochFns.processHistoricalRootsUpdate, inactivity_updates: epochFns.processInactivityUpdates as EpochTransitionFn, @@ -30,7 +33,10 @@ const epochTransitionFns: Record = { participation_flag_updates: epochFns.processParticipationFlagUpdates as EpochTransitionFn, participation_record_updates: epochFns.processParticipationRecordUpdates as EpochTransitionFn, randao_mixes_reset: epochFns.processRandaoMixesReset, - registry_updates: epochFns.processRegistryUpdates, + registry_updates: (state, epochTransitionCache) => { + const fork = state.config.getForkSeq(state.slot); + epochFns.processRegistryUpdates(fork, state, epochTransitionCache); + }, rewards_and_penalties: epochFns.processRewardsAndPenalties, slashings: epochFns.processSlashings, slashings_reset: epochFns.processSlashingsReset, diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index c2934b55dd62..63d89178bb55 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -71,7 +71,8 @@ const operationFns: Record> = sync_aggregate_random: sync_aggregate, voluntary_exit: (state, testCase: {voluntary_exit: phase0.SignedVoluntaryExit}) => { - blockFns.processVoluntaryExit(state, testCase.voluntary_exit); + const fork = state.config.getForkSeq(state.slot); + blockFns.processVoluntaryExit(fork, state, testCase.voluntary_exit); }, execution_payload: (state, testCase: {body: bellatrix.BeaconBlockBody; execution: {execution_valid: boolean}}) => { diff --git a/packages/types/src/electra/index.ts b/packages/types/src/electra/index.ts index 7856cd729620..981b2015e02a 100644 --- a/packages/types/src/electra/index.ts +++ b/packages/types/src/electra/index.ts @@ -1,3 +1,4 @@ export * from "./types.js"; -export * as ts from "./types.js"; -export * as ssz from "./sszTypes.js"; +import * as ts from "./types.js"; +import * as ssz from "./sszTypes.js"; +export {ts, ssz}; From 96f770126ee6d6a89447db0a2ed3894096b35a5b Mon Sep 17 00:00:00 2001 From: NC Date: Tue, 14 May 2024 05:52:02 +0300 Subject: [PATCH 23/51] fix: fix electra genesis spec test (#6764) * process pending deposit from eth1 * Fix the genesis params * fix * Fix * clean up --------- Co-authored-by: Nazar Hussain --- .../beacon-node/src/chain/genesis/genesis.ts | 8 ++++- packages/state-transition/src/util/genesis.ts | 29 ++++++++++++++----- packages/types/src/electra/sszTypes.ts | 7 ++++- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/beacon-node/src/chain/genesis/genesis.ts b/packages/beacon-node/src/chain/genesis/genesis.ts index 979476c69530..5efd352357eb 100644 --- a/packages/beacon-node/src/chain/genesis/genesis.ts +++ b/packages/beacon-node/src/chain/genesis/genesis.ts @@ -174,7 +174,13 @@ export class GenesisBuilder implements IGenesisBuilder { }; }); - const {activatedValidatorCount} = applyDeposits(this.config, this.state, newDeposits, this.depositTree); + const {activatedValidatorCount} = applyDeposits( + this.config.getForkSeq(this.state.slot), + this.config, + this.state, + newDeposits, + this.depositTree + ); this.activatedValidatorCount += activatedValidatorCount; // TODO: If necessary persist deposits here to this.db.depositData, this.db.depositDataRoot diff --git a/packages/state-transition/src/util/genesis.ts b/packages/state-transition/src/util/genesis.ts index d5f4c577711f..ca894fd5b4e5 100644 --- a/packages/state-transition/src/util/genesis.ts +++ b/packages/state-transition/src/util/genesis.ts @@ -4,6 +4,7 @@ import { EFFECTIVE_BALANCE_INCREMENT, EPOCHS_PER_HISTORICAL_VECTOR, ForkName, + ForkSeq, GENESIS_EPOCH, GENESIS_SLOT, MAX_EFFECTIVE_BALANCE, @@ -11,10 +12,11 @@ import { } from "@lodestar/params"; import {Bytes32, phase0, Root, ssz, TimeSeconds} from "@lodestar/types"; -import {CachedBeaconStateAllForks, BeaconStateAllForks} from "../types.js"; +import {CachedBeaconStateAllForks, BeaconStateAllForks, CachedBeaconStateElectra} from "../types.js"; import {createCachedBeaconState} from "../cache/stateCache.js"; import {EpochCacheImmutableData} from "../cache/epochCache.js"; import {processDeposit} from "../block/processDeposit.js"; +import {increaseBalance} from "../index.js"; import {computeEpochAtSlot} from "./epoch.js"; import {getActiveValidatorIndices} from "./validator.js"; import {getTemporaryBlockHeader} from "./blockRoot.js"; @@ -127,6 +129,7 @@ export function applyTimestamp(config: ChainForkConfig, state: CachedBeaconState * @returns active validator indices */ export function applyDeposits( + fork: ForkSeq, config: ChainForkConfig, state: CachedBeaconStateAllForks, newDeposits: phase0.Deposit[], @@ -164,6 +167,16 @@ export function applyDeposits( processDeposit(fork, state, deposit); } + // Process deposit balance updates + if (fork >= ForkSeq.electra) { + const stateElectra = state as CachedBeaconStateElectra; + stateElectra.commit(); + for (const {index: validatorIndex, amount} of stateElectra.pendingBalanceDeposits.getAllReadonly()) { + increaseBalance(state, validatorIndex, Number(amount)); + } + stateElectra.pendingBalanceDeposits = ssz.electra.PendingBalanceDeposits.defaultViewDU(); + } + // Process activations const {epochCtx} = state; const balancesArr = state.balances.getAll(); @@ -226,6 +239,8 @@ export function initializeBeaconStateFromEth1( getTemporaryBlockHeader(config, config.getForkTypes(GENESIS_SLOT).BeaconBlock.defaultValue()) ); + const fork = config.getForkSeq(GENESIS_SLOT); + // We need a CachedBeaconState to run processDeposit() which uses various caches. // However at this point the state's syncCommittees are not known. // This function can be called by: @@ -240,13 +255,13 @@ export function initializeBeaconStateFromEth1( applyEth1BlockHash(state, eth1BlockHash); // Process deposits - applyDeposits(config, state, deposits, fullDepositDataRootList); + applyDeposits(fork, config, state, deposits, fullDepositDataRootList); // Commit before reading all validators in `getActiveValidatorIndices()` state.commit(); const activeValidatorIndices = getActiveValidatorIndices(state, computeEpochAtSlot(GENESIS_SLOT)); - if (GENESIS_SLOT >= config.ALTAIR_FORK_EPOCH) { + if (fork >= ForkSeq.altair) { const {syncCommittee} = getNextSyncCommittee( state, activeValidatorIndices, @@ -259,7 +274,7 @@ export function initializeBeaconStateFromEth1( stateAltair.nextSyncCommittee = ssz.altair.SyncCommittee.toViewDU(syncCommittee); } - if (GENESIS_SLOT >= config.BELLATRIX_FORK_EPOCH) { + if (fork >= ForkSeq.bellatrix) { const stateBellatrix = state as CompositeViewDU; stateBellatrix.fork.previousVersion = config.BELLATRIX_FORK_VERSION; stateBellatrix.fork.currentVersion = config.BELLATRIX_FORK_VERSION; @@ -268,7 +283,7 @@ export function initializeBeaconStateFromEth1( ssz.bellatrix.ExecutionPayloadHeader.defaultViewDU(); } - if (GENESIS_SLOT >= config.CAPELLA_FORK_EPOCH) { + if (fork >= ForkSeq.capella) { const stateCapella = state as CompositeViewDU; stateCapella.fork.previousVersion = config.CAPELLA_FORK_VERSION; stateCapella.fork.currentVersion = config.CAPELLA_FORK_VERSION; @@ -277,7 +292,7 @@ export function initializeBeaconStateFromEth1( ssz.capella.ExecutionPayloadHeader.defaultViewDU(); } - if (GENESIS_SLOT >= config.DENEB_FORK_EPOCH) { + if (fork >= ForkSeq.deneb) { const stateDeneb = state as CompositeViewDU; stateDeneb.fork.previousVersion = config.DENEB_FORK_VERSION; stateDeneb.fork.currentVersion = config.DENEB_FORK_VERSION; @@ -286,7 +301,7 @@ export function initializeBeaconStateFromEth1( ssz.deneb.ExecutionPayloadHeader.defaultViewDU(); } - if (GENESIS_SLOT >= config.ELECTRA_FORK_EPOCH) { + if (fork >= ForkSeq.electra) { const stateElectra = state as CompositeViewDU; stateElectra.fork.previousVersion = config.ELECTRA_FORK_VERSION; stateElectra.fork.currentVersion = config.ELECTRA_FORK_VERSION; diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index ee8d2702c65d..d3d9843e63f1 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -275,6 +275,11 @@ export const PendingBalanceDeposit = new ContainerType( {typeName: "PendingBalanceDeposit", jsonCase: "eth2"} ); +export const PendingBalanceDeposits = new ListCompositeType( + PendingBalanceDeposit, + Number(PENDING_BALANCE_DEPOSITS_LIMIT) +); + export const PartialWithdrawal = new ContainerType( { index: ValidatorIndex, @@ -341,7 +346,7 @@ export const BeaconState = new ContainerType( earliestExitEpoch: Epoch, // New in Electra:EIP7251 consolidationBalanceToConsume: Gwei, // New in Electra:EIP7251 earliestConsolidationEpoch: Epoch, // New in Electra:EIP7251 - pendingBalanceDeposits: new ListCompositeType(PendingBalanceDeposit, Number(PENDING_BALANCE_DEPOSITS_LIMIT)), // new in electra:eip7251 + pendingBalanceDeposits: PendingBalanceDeposits, // new in electra:eip7251 pendingPartialWithdrawals: new ListCompositeType(PartialWithdrawal, Number(PENDING_PARTIAL_WITHDRAWALS_LIMIT)), // New in Electra:EIP7251 pendingConsolidations: new ListCompositeType(PendingConsolidation, Number(PENDING_CONSOLIDATIONS_LIMIT)), // new in electra:eip7251 }, From 41b857adbdadd583ce1c14c12008a2c9d028d180 Mon Sep 17 00:00:00 2001 From: NC Date: Tue, 14 May 2024 06:39:55 +0300 Subject: [PATCH 24/51] feat: support missing electra spec test (#6765) Add spec test --- .../test/spec/presets/epoch_processing.test.ts | 2 ++ .../test/spec/presets/operations.test.ts | 17 +++++++++++++++++ .../src/block/processOperations.ts | 1 + 3 files changed, 20 insertions(+) diff --git a/packages/beacon-node/test/spec/presets/epoch_processing.test.ts b/packages/beacon-node/test/spec/presets/epoch_processing.test.ts index b089863fdad3..f159612e416c 100644 --- a/packages/beacon-node/test/spec/presets/epoch_processing.test.ts +++ b/packages/beacon-node/test/spec/presets/epoch_processing.test.ts @@ -42,6 +42,8 @@ const epochTransitionFns: Record = { slashings_reset: epochFns.processSlashingsReset, sync_committee_updates: epochFns.processSyncCommitteeUpdates as EpochTransitionFn, historical_summaries_update: epochFns.processHistoricalSummariesUpdate as EpochTransitionFn, + pending_balance_deposits: epochFns.processPendingBalanceDeposits as EpochTransitionFn, + pending_consolidations: epochFns.processPendingConsolidations as EpochTransitionFn, }; /** diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index 63d89178bb55..228f6fd033d7 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -91,6 +91,21 @@ const operationFns: Record> = withdrawals: (state, testCase: {execution_payload: capella.ExecutionPayload}) => { blockFns.processWithdrawals(ForkSeq.capella, state as CachedBeaconStateCapella, testCase.execution_payload); }, + + consolidation: (state, testCase: {consolidation: electra.SignedConsolidation}) => { + blockFns.processConsolidation(state as CachedBeaconStateElectra, testCase.consolidation); + }, + + execution_layer_withdrawal_request: ( + state, + testCase: {execution_layer_withdrawal_request: electra.ExecutionLayerWithdrawalRequest} + ) => { + blockFns.processExecutionLayerWithdrawalRequest( + ForkSeq.electra, + state as CachedBeaconStateElectra, + testCase.execution_layer_withdrawal_request + ); + }, }; export type BlockProcessFn = (state: T, testCase: any) => void; @@ -140,6 +155,8 @@ const operations: TestRunnerFn = (fork, : ssz.bellatrix.ExecutionPayload, // Capella address_change: ssz.capella.SignedBLSToExecutionChange, + consolidation: ssz.electra.SignedConsolidation, + execution_layer_withdrawal_request: ssz.electra.ExecutionLayerWithdrawalRequest, }, shouldError: (testCase) => testCase.post === undefined, getExpected: (testCase) => testCase.post, diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index 67ec549a7715..f996304e9f78 100644 --- a/packages/state-transition/src/block/processOperations.ts +++ b/packages/state-transition/src/block/processOperations.ts @@ -23,6 +23,7 @@ export { processExecutionLayerWithdrawalRequest, processBlsToExecutionChange, processDepositRequest, + processConsolidation, }; export function processOperations( From 4fc06affa37a0fe30011d6978d873d13251defb0 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 14 May 2024 10:37:56 +0300 Subject: [PATCH 25/51] test: fix ssz types in fork_choice spec tests (#6767) --- packages/beacon-node/test/spec/presets/fork_choice.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index 98471dffba39..5e4f383fd7c5 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -343,8 +343,8 @@ const forkChoiceTest = [BLOCK_FILE_NAME]: ssz[fork].SignedBeaconBlock, [BLOBS_FILE_NAME]: ssz.deneb.Blobs, [POW_BLOCK_FILE_NAME]: ssz.bellatrix.PowBlock, - [ATTESTATION_FILE_NAME]: ssz.phase0.Attestation, - [ATTESTER_SLASHING_FILE_NAME]: ssz.phase0.AttesterSlashing, + [ATTESTATION_FILE_NAME]: ssz.allForks[fork].Attestation, + [ATTESTER_SLASHING_FILE_NAME]: ssz.allForks[fork].AttesterSlashing, }, mapToTestCase: (t: Record) => { // t has input file name as key From 27cf29a3dd3af0d5fa418b42b71d58103eca004b Mon Sep 17 00:00:00 2001 From: Julien Date: Tue, 14 May 2024 00:50:29 -0700 Subject: [PATCH 26/51] chore: update EffectiveBalanceIncrements type (#6763) * chore: update EffectiveBalanceIncrements type * chore: remove now irrelevant tests --- packages/state-transition/package.json | 1 - .../src/cache/effectiveBalanceIncrements.ts | 13 ++-- .../state-transition/src/cache/epochCache.ts | 8 +-- packages/state-transition/src/util/balance.ts | 4 +- .../test/memory/effectiveBalanceIncrements.ts | 62 ------------------- .../perf/dataStructures/arrayish.memory.ts | 54 ---------------- .../test/perf/dataStructures/arrayish.test.ts | 43 ------------- .../effectiveBalanceIncrements.test.ts | 32 ---------- yarn.lock | 27 +------- 9 files changed, 14 insertions(+), 230 deletions(-) delete mode 100644 packages/state-transition/test/memory/effectiveBalanceIncrements.ts delete mode 100644 packages/state-transition/test/perf/dataStructures/effectiveBalanceIncrements.test.ts diff --git a/packages/state-transition/package.json b/packages/state-transition/package.json index 5a3b44f507f2..3a27cc8986c2 100644 --- a/packages/state-transition/package.json +++ b/packages/state-transition/package.json @@ -62,7 +62,6 @@ "@chainsafe/bls": "^8.1.0", "@chainsafe/blst": "^1.0.0", "@chainsafe/persistent-merkle-tree": "^0.7.1", - "@chainsafe/persistent-ts": "^0.19.1", "@chainsafe/ssz": "^0.16.0", "@lodestar/config": "^1.19.0", "@lodestar/params": "^1.19.0", diff --git a/packages/state-transition/src/cache/effectiveBalanceIncrements.ts b/packages/state-transition/src/cache/effectiveBalanceIncrements.ts index a82eb0300432..bd72b333a03c 100644 --- a/packages/state-transition/src/cache/effectiveBalanceIncrements.ts +++ b/packages/state-transition/src/cache/effectiveBalanceIncrements.ts @@ -3,18 +3,17 @@ import {BeaconStateAllForks} from "../types.js"; /** * Alias to allow easier refactoring. - * TODO: Estimate the risk of future proof of MAX_EFFECTIVE_BALANCE_INCREMENT < 255 */ -export type EffectiveBalanceIncrements = Uint8Array; +export type EffectiveBalanceIncrements = Uint16Array; -/** Helper to prevent re-writting tests downstream if we change Uint8Array to number[] */ +/** Helper to prevent re-writting tests downstream if we change Uint16Array to number[] */ export function getEffectiveBalanceIncrementsZeroed(len: number): EffectiveBalanceIncrements { - return new Uint8Array(len); + return new Uint16Array(len); } /** * effectiveBalanceIncrements length will always be equal or greater than validatorCount. The - * getEffectiveBalanceIncrementsByteLen() modulo is used to reduce the frequency at which its Uint8Array is recreated. + * getEffectiveBalanceIncrementsByteLen() modulo is used to reduce the frequency at which its Uint16Array is recreated. * if effectiveBalanceIncrements has length greater than validatorCount it's not a problem since those values would * never be accessed. */ @@ -22,7 +21,7 @@ export function getEffectiveBalanceIncrementsWithLen(validatorCount: number): Ef // TODO: Research what's the best number to minimize both memory cost and copy costs const byteLen = 1024 * Math.ceil(validatorCount / 1024); - return new Uint8Array(byteLen); + return new Uint16Array(byteLen); } /** @@ -32,7 +31,7 @@ export function getEffectiveBalanceIncrementsWithLen(validatorCount: number): Ef */ export function getEffectiveBalanceIncrements(state: BeaconStateAllForks): EffectiveBalanceIncrements { const validatorsArr = state.validators.getAllReadonlyValues(); - const effectiveBalanceIncrements = new Uint8Array(validatorsArr.length); + const effectiveBalanceIncrements = new Uint16Array(validatorsArr.length); for (let i = 0; i < validatorsArr.length; i++) { effectiveBalanceIncrements[i] = Math.floor(validatorsArr[i].effectiveBalance / EFFECTIVE_BALANCE_INCREMENT); } diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index af1362b55a7e..b03576a653ec 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -647,8 +647,8 @@ export class EpochCache { beforeEpochTransition(): void { // Clone (copy) before being mutated in processEffectiveBalanceUpdates - // NOTE: Force to use Uint8Array.slice (copy) instead of Buffer.call (not copy) - this.effectiveBalanceIncrements = Uint8Array.prototype.slice.call(this.effectiveBalanceIncrements, 0); + // NOTE: Force to use Uint16Array.slice (copy) instead of Buffer.call (not copy) + this.effectiveBalanceIncrements = Uint16Array.prototype.slice.call(this.effectiveBalanceIncrements, 0); } /** @@ -1042,13 +1042,13 @@ export class EpochCache { const newLength = index >= this.effectiveBalanceIncrements.length ? index + 1 : this.effectiveBalanceIncrements.length; const effectiveBalanceIncrements = this.effectiveBalanceIncrements; - this.effectiveBalanceIncrements = new Uint8Array(newLength); + this.effectiveBalanceIncrements = new Uint16Array(newLength); this.effectiveBalanceIncrements.set(effectiveBalanceIncrements, 0); } else { if (index >= this.effectiveBalanceIncrements.length) { // Clone and extend effectiveBalanceIncrements const effectiveBalanceIncrements = this.effectiveBalanceIncrements; - this.effectiveBalanceIncrements = new Uint8Array(getEffectiveBalanceIncrementsByteLen(index + 1)); + this.effectiveBalanceIncrements = new Uint16Array(getEffectiveBalanceIncrementsByteLen(index + 1)); this.effectiveBalanceIncrements.set(effectiveBalanceIncrements, 0); } } diff --git a/packages/state-transition/src/util/balance.ts b/packages/state-transition/src/util/balance.ts index e305c745ab72..e9b7a06e4130 100644 --- a/packages/state-transition/src/util/balance.ts +++ b/packages/state-transition/src/util/balance.ts @@ -56,8 +56,8 @@ export function getEffectiveBalanceIncrementsZeroInactive( const validatorCount = justifiedState.validators.length; const {effectiveBalanceIncrements} = justifiedState.epochCtx; // Slice up to `validatorCount` since it won't be mutated, nor accessed beyond `validatorCount` - // NOTE: Force to use Uint8Array.slice (copy) instead of Buffer.call (not copy) - const effectiveBalanceIncrementsZeroInactive = Uint8Array.prototype.slice.call( + // NOTE: Force to use Uint16Array.slice (copy) instead of Buffer.call (not copy) + const effectiveBalanceIncrementsZeroInactive = Uint16Array.prototype.slice.call( effectiveBalanceIncrements, 0, validatorCount diff --git a/packages/state-transition/test/memory/effectiveBalanceIncrements.ts b/packages/state-transition/test/memory/effectiveBalanceIncrements.ts deleted file mode 100644 index f1c603b85657..000000000000 --- a/packages/state-transition/test/memory/effectiveBalanceIncrements.ts +++ /dev/null @@ -1,62 +0,0 @@ -import {MutableVector} from "@chainsafe/persistent-ts"; -import {testRunnerMemory} from "@lodestar/beacon-node/test/memory/testRunnerMemory"; -import {newZeroedArray} from "../../src/index.js"; - -// Results in Linux Feb 2022 -// -// EffectiveBalanceIncrements Uint8Array 300000 - 299873.5 bytes / instance -// EffectiveBalanceIncrements array 300000 - 2400093.1 bytes / instance -// EffectiveBalanceIncrements MutableVector 300000 - 4380557.0 bytes / instance -// EffectiveBalanceIncrements MutableVector 300000 cloned 10 - 4399575.0 bytes / instance -// -// With MutableVector, break even at 14 instances of Uint8Array -// 4380557 / 299873 = 14 - -const vc = 300_000; -const cloneTimes = 10; - -testRunnerMemoryBpi([ - { - id: `EffectiveBalanceIncrements Uint8Array ${vc}`, - getInstance: () => new Uint8Array(vc), - }, - { - id: `EffectiveBalanceIncrements array ${vc}`, - getInstance: () => newZeroedArray(vc), - }, - { - id: `EffectiveBalanceIncrements MutableVector ${vc}`, - getInstance: () => MutableVector.from(newZeroedArray(vc)), - }, - { - id: `EffectiveBalanceIncrements MutableVector ${vc} cloned ${cloneTimes}`, - getInstance: () => { - const mv = MutableVector.from(newZeroedArray(vc)); - const mvs = [mv]; - for (let i = 0; i < cloneTimes; i++) { - const mvc = mv.clone(); - mvc.push(0); - mvs.push(mvc); - } - return mvs; - }, - }, -]); - -/** - * Test bytes per instance in different representations of raw binary data - */ -function testRunnerMemoryBpi(testCases: {getInstance: (bytes: number) => unknown; id: string}[]): void { - const longestId = Math.max(...testCases.map(({id}) => id.length)); - - for (const {id, getInstance} of testCases) { - const bpi = testRunnerMemory({ - getInstance, - convergeFactor: 1 / 100, - sampleEvery: 5, - }); - - // eslint-disable-next-line no-console - console.log(`${id.padEnd(longestId)} - ${bpi.toFixed(1)} bytes / instance`); - } -} diff --git a/packages/state-transition/test/perf/dataStructures/arrayish.memory.ts b/packages/state-transition/test/perf/dataStructures/arrayish.memory.ts index 1f4141912627..7e10f447181f 100644 --- a/packages/state-transition/test/perf/dataStructures/arrayish.memory.ts +++ b/packages/state-transition/test/perf/dataStructures/arrayish.memory.ts @@ -1,5 +1,3 @@ -import {MutableVector} from "@chainsafe/persistent-ts"; - const refs: any[] = []; const xs: number[] = []; const arrayBuffersArr: number[] = []; @@ -23,7 +21,6 @@ const size = 100; const testType = TestType.Set; let arrayNumGlobal: number[] | null = null; -let mutableVectorGlobal: MutableVector | null = null; for (let i = 0; i < 1e8; i++) { switch (testType as TestType) { @@ -65,49 +62,6 @@ for (let i = 0; i < 1e8; i++) { break; } - // size | 100 | 1000 | 10000 | - // ---- | ------ | ------ | ------ | - // rssM | 1817.4 | 15518. | 154335 | - case TestType.MutableVector: { - const items = createArray(size); - const mutableVector = MutableVector.from(items); - refs.push(mutableVector); - break; - } - - // size | 100 | 1000 | - // ---- | ------ | ------ | - // rssM | 58.68 | 55.89 | - case TestType.MutableVectorClone: { - if (!mutableVectorGlobal) { - const items = createArray(size); - mutableVectorGlobal = MutableVector.from(items); - } - refs.push(mutableVectorGlobal.clone()); - break; - } - - // Grid of size / changes, all values = rssM in bytes - // | 100 | 1000 | 10000 | - // ----- | ------ | ------ | ------ | - // 1 | 793.45 | 801.53 | 1137.9 | - // 10 | 803.98 | 802.36 | 1144.9 | - // 100 | 1573.2 | 1826.4 | 2172.0 | - // 1000 | - | 11250. | 11886. | - // 10000 | - | - | 111365 | - case TestType.MutableVectorCloneAndMutate: { - if (!mutableVectorGlobal) { - const items = createArray(size); - mutableVectorGlobal = MutableVector.from(items); - } - const newArr = mutableVectorGlobal.clone(); - for (let j = 0; j < 10000; j++) { - newArr.set(j, i); - } - refs.push(newArr); - break; - } - // size | 100 | 1000 | // ---- | ------ | ------ | // rssM | 2646.8 | 20855. | @@ -161,14 +115,6 @@ for (let i = 0; i < 1e8; i++) { } } -function createArray(n: number): number[] { - const items: number[] = []; - for (let i = 0; i < n; i++) { - items.push(i); - } - return items; -} - /** * From https://github.com/simple-statistics/simple-statistics/blob/d0d177baf74976a2421638bce98ab028c5afb537/src/linear_regression.js * diff --git a/packages/state-transition/test/perf/dataStructures/arrayish.test.ts b/packages/state-transition/test/perf/dataStructures/arrayish.test.ts index 59162b6eecca..5b6af0d989b6 100644 --- a/packages/state-transition/test/perf/dataStructures/arrayish.test.ts +++ b/packages/state-transition/test/perf/dataStructures/arrayish.test.ts @@ -1,6 +1,5 @@ import {itBench, setBenchOpts} from "@dapplion/benchmark"; import {LeafNode, toGindex, Tree, zeroNode} from "@chainsafe/persistent-merkle-tree"; -import {MutableVector} from "@chainsafe/persistent-ts"; // Understand the cost of each array-ish data structure to: // - Get one element @@ -99,48 +98,6 @@ describe("Tree (persistent-merkle-tree)", () => { } }); -describe("MutableVector", () => { - // Don't track regressions in CI - setBenchOpts({noThreshold: true}); - - let items: number[]; - let mutableVector: MutableVector; - - before(function () { - items = createArray(n); - mutableVector = MutableVector.from(items); - }); - - itBench(`MutableVector ${n} create`, () => { - MutableVector.from(items); - }); - - itBench({id: `MutableVector ${n} get(${ih})`, runsFactor}, () => { - for (let i = 0; i < runsFactor; i++) mutableVector.get(ih - i); - }); - - itBench({id: `MutableVector ${n} set(${ih})`, runsFactor}, () => { - for (let i = 0; i < runsFactor; i++) mutableVector.set(ih - i, 10000000); - }); - - itBench(`MutableVector ${n} toArray()`, () => { - mutableVector.toArray(); - }); - - itBench(`MutableVector ${n} iterate all - toArray() + loop`, () => { - const mvArr = mutableVector.toArray(); - for (let i = 0; i < n; i++) { - mvArr[i]; - } - }); - - itBench(`MutableVector ${n} iterate all - get(i)`, () => { - for (let i = 0; i < n; i++) { - mutableVector.get(i); - } - }); -}); - describe("Array", () => { // Don't track regressions in CI setBenchOpts({noThreshold: true}); diff --git a/packages/state-transition/test/perf/dataStructures/effectiveBalanceIncrements.test.ts b/packages/state-transition/test/perf/dataStructures/effectiveBalanceIncrements.test.ts deleted file mode 100644 index 13c2d982e86b..000000000000 --- a/packages/state-transition/test/perf/dataStructures/effectiveBalanceIncrements.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {itBench, setBenchOpts} from "@dapplion/benchmark"; -import {MutableVector} from "@chainsafe/persistent-ts"; -import {newZeroedArray} from "../../../src/index.js"; - -describe("effectiveBalanceIncrements", () => { - setBenchOpts({noThreshold: true}); - - const vc = 300_000; - const uint8Array = new Uint8Array(vc); - const mv = MutableVector.from(newZeroedArray(vc)); - - itBench(`effectiveBalanceIncrements clone Uint8Array ${vc}`, () => { - uint8Array.slice(0); - }); - - itBench(`effectiveBalanceIncrements clone MutableVector ${vc}`, () => { - mv.clone(); - }); - - itBench(`effectiveBalanceIncrements rw all Uint8Array ${vc}`, () => { - for (let i = 0; i < vc; i++) { - uint8Array[i]++; - } - }); - - itBench(`effectiveBalanceIncrements rw all MutableVector ${vc}`, () => { - for (let i = 0; i < vc; i++) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - mv.set(i, mv.get(i)! + 1); - } - }); -}); diff --git a/yarn.lock b/yarn.lock index 98fc4c71e033..e70da22818ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -472,11 +472,6 @@ "@chainsafe/as-sha256" "^0.4.2" "@noble/hashes" "^1.3.0" -"@chainsafe/persistent-ts@^0.19.1": - version "0.19.1" - resolved "https://registry.npmjs.org/@chainsafe/persistent-ts/-/persistent-ts-0.19.1.tgz" - integrity sha512-fUFFFFxdcpYkMAHnjm83EYL/R/smtVmEkJr3FGSI6dwPk4ue9rXjEHf7FTd3V8AbVOcTJGriN4cYf2V+HOYkjQ== - "@chainsafe/prometheus-gc-stats@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@chainsafe/prometheus-gc-stats/-/prometheus-gc-stats-1.0.2.tgz#585f8f1555251db156d7e50ef8c86dd4f3e78f70" @@ -11807,16 +11802,7 @@ string-argv@~0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13451,16 +13437,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From c476b944d448a8ab8e212f4bd71973bee7976955 Mon Sep 17 00:00:00 2001 From: Navie Chan Date: Tue, 14 May 2024 11:37:56 +0300 Subject: [PATCH 27/51] Fix ssz_static --- packages/beacon-node/src/chain/chain.ts | 2 +- .../src/eth1/eth1DepositDataTracker.ts | 4 +-- .../beacon-node/src/execution/engine/types.ts | 6 ++-- .../test/sim/electra-interop.test.ts | 4 +-- .../test/spec/presets/ssz_static.test.ts | 1 + packages/beacon-node/test/utils/state.ts | 2 +- packages/light-client/src/spec/utils.ts | 6 ++-- packages/params/src/index.ts | 4 +++ .../src/block/processDepositRequest.ts | 4 +-- .../processExecutionLayerWithdrawalRequest.ts | 2 +- .../src/block/processOperations.ts | 2 +- .../src/slot/upgradeStateToElectra.ts | 14 +++++----- packages/state-transition/src/util/deposit.ts | 4 +-- .../state-transition/src/util/execution.ts | 4 +-- packages/state-transition/src/util/genesis.ts | 2 +- .../test/unit/util/deposit.test.ts | 4 +-- packages/types/src/electra/sszTypes.ts | 28 ++++++++++--------- packages/types/src/electra/types.ts | 6 ++-- 18 files changed, 53 insertions(+), 46 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index f8007d12aa8b..25b72c57f769 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -1093,7 +1093,7 @@ export class BeaconChain implements IBeaconChain { // Will resolve this later // if (cpEpoch >= (this.config.ELECTRA_FORK_EPOCH ?? Infinity)) { // // finalizedState can be safely casted to Electra state since cp is already post-Electra - // if (finalizedState.eth1DepositIndex >= (finalizedState as CachedBeaconStateElectra).depositRequestsStartIndex) { + // if (finalizedState.eth1DepositIndex >= (finalizedState as CachedBeaconStateElectra).depositReceiptsStartIndex) { // // Signal eth1 to stop polling eth1Data // this.eth1.stopPollingEth1Data(); // } diff --git a/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts b/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts index d0578718f29f..c0b3ab35a73a 100644 --- a/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts +++ b/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts @@ -129,9 +129,9 @@ export class Eth1DepositDataTracker { async getEth1DataAndDeposits(state: CachedBeaconStateAllForks): Promise { if ( state.epochCtx.isAfterElectra() && - state.eth1DepositIndex >= (state as CachedBeaconStateElectra).depositRequestsStartIndex + state.eth1DepositIndex >= (state as CachedBeaconStateElectra).depositReceiptsStartIndex ) { - // No need to poll eth1Data since Electra deprecates the mechanism after depositRequestsStartIndex is reached + // No need to poll eth1Data since Electra deprecates the mechanism after depositReceiptsStartIndex is reached return {eth1Data: state.eth1Data, deposits: []}; } const eth1Data = this.forcedEth1DataVote ?? (await this.getEth1Data(state)); diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index 71e8e2fc77a5..43c269d1c402 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -217,8 +217,8 @@ export function serializeExecutionPayload(fork: ForkName, data: allForks.Executi // ELECTRA adds depositRequests/depositRequests to the ExecutionPayload if (ForkSeq[fork] >= ForkSeq.electra) { - const {depositRequests, withdrawalRequests} = data as electra.ExecutionPayload; - payload.depositRequests = depositRequests.map(serializeDepositRequest); + const {depositReceipts, withdrawalRequests} = data as electra.ExecutionPayload; + payload.depositRequests = depositReceipts.map(serializeDepositRequest); payload.withdrawalRequests = withdrawalRequests.map(serializeExecutionLayerWithdrawalRequest); } @@ -316,7 +316,7 @@ export function parseExecutionPayload( `depositRequests missing for ${fork} >= electra executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}` ); } - (executionPayload as electra.ExecutionPayload).depositRequests = depositRequests.map(deserializeDepositRequest); + (executionPayload as electra.ExecutionPayload).depositReceipts = depositRequests.map(deserializeDepositRequest); if (withdrawalRequests == null) { throw Error( diff --git a/packages/beacon-node/test/sim/electra-interop.test.ts b/packages/beacon-node/test/sim/electra-interop.test.ts index 2dfa5ee0f77c..37bed1f7b843 100644 --- a/packages/beacon-node/test/sim/electra-interop.test.ts +++ b/packages/beacon-node/test/sim/electra-interop.test.ts @@ -431,8 +431,8 @@ describe("executionEngine / ExecutionEngineHttp", function () { throw Error("Historical validator length for epoch 1 or 2 is not dropped properly"); } - if (headState.depositRequestsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { - throw Error("state.depositRequestsStartIndex is not set upon processing new deposit receipt"); + if (headState.depositReceiptsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { + throw Error("state.depositReceiptsStartIndex is not set upon processing new deposit receipt"); } // wait for 1 slot to print current epoch stats diff --git a/packages/beacon-node/test/spec/presets/ssz_static.test.ts b/packages/beacon-node/test/spec/presets/ssz_static.test.ts index c77a3377c433..468ad7ab7144 100644 --- a/packages/beacon-node/test/spec/presets/ssz_static.test.ts +++ b/packages/beacon-node/test/spec/presets/ssz_static.test.ts @@ -48,6 +48,7 @@ const sszStatic = // will help us get the right type for lightclient objects ((ssz.allForksLightClient[fork as ForkLightClient] || {}) as Types)[typeName] || (ssz[fork] as Types)[typeName] || + (ssz.electra as Types)[typeName] || (ssz.deneb as Types)[typeName] || (ssz.capella as Types)[typeName] || (ssz.bellatrix as Types)[typeName] || diff --git a/packages/beacon-node/test/utils/state.ts b/packages/beacon-node/test/utils/state.ts index 70b50c86004b..04e1dc710b5e 100644 --- a/packages/beacon-node/test/utils/state.ts +++ b/packages/beacon-node/test/utils/state.ts @@ -97,7 +97,7 @@ export function generateState( if (forkSeq >= ForkSeq.electra) { const stateElectra = state as electra.BeaconState; - stateElectra.depositRequestsStartIndex = 2023n; + stateElectra.depositReceiptsStartIndex = 2023n; stateElectra.latestExecutionPayloadHeader = ssz.electra.ExecutionPayloadHeader.defaultValue(); } diff --git a/packages/light-client/src/spec/utils.ts b/packages/light-client/src/spec/utils.ts index 027ef87dda48..0c1dccbb7038 100644 --- a/packages/light-client/src/spec/utils.ts +++ b/packages/light-client/src/spec/utils.ts @@ -106,8 +106,8 @@ export function upgradeLightClientHeader( // eslint-disable-next-line no-fallthrough case ForkName.electra: - (upgradedHeader as electra.LightClientHeader).execution.depositRequestsRoot = - ssz.electra.LightClientHeader.fields.execution.fields.depositRequestsRoot.defaultValue(); + (upgradedHeader as electra.LightClientHeader).execution.depositReceiptsRoot = + ssz.electra.LightClientHeader.fields.execution.fields.depositReceiptsRoot.defaultValue(); (upgradedHeader as electra.LightClientHeader).execution.withdrawalRequestsRoot = ssz.electra.LightClientHeader.fields.execution.fields.withdrawalRequestsRoot.defaultValue(); @@ -148,7 +148,7 @@ export function isValidLightClientHeader(config: ChainForkConfig, header: allFor if (epoch < config.ELECTRA_FORK_EPOCH) { if ( - (header as electra.LightClientHeader).execution.depositRequestsRoot !== undefined || + (header as electra.LightClientHeader).execution.depositReceiptsRoot !== undefined || (header as electra.LightClientHeader).execution.withdrawalRequestsRoot !== undefined ) { return false; diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index c1d3f1bc1981..20439568156e 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -264,3 +264,7 @@ export const BLOBSIDECAR_FIXED_SIZE = ACTIVE_PRESET === PresetName.minimal ? 131 // Electra Misc export const UNSET_DEPOSIT_RECEIPTS_START_INDEX = 2n ** 64n - 1n; export const FULL_EXIT_REQUEST_AMOUNT = 0; +export const NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA = 87; +export const NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA = 6; +export const FINALIZED_ROOT_DEPTH_ELECTRA = 7; +export const FINALIZED_ROOT_INDEX_ELECTRA = 169; \ No newline at end of file diff --git a/packages/state-transition/src/block/processDepositRequest.ts b/packages/state-transition/src/block/processDepositRequest.ts index ca6fe4206188..d5525f3cd544 100644 --- a/packages/state-transition/src/block/processDepositRequest.ts +++ b/packages/state-transition/src/block/processDepositRequest.ts @@ -9,8 +9,8 @@ export function processDepositRequest( state: CachedBeaconStateElectra, depositRequest: electra.DepositRequest ): void { - if (state.depositRequestsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { - state.depositRequestsStartIndex = BigInt(depositRequest.index); + if (state.depositReceiptsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { + state.depositReceiptsStartIndex = BigInt(depositRequest.index); } applyDeposit(fork, state, depositRequest); diff --git a/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts b/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts index e16fad105aaa..1b795789cb77 100644 --- a/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts +++ b/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts @@ -70,7 +70,7 @@ export function processExecutionLayerWithdrawalRequest( const exitQueueEpoch = computeExitEpochAndUpdateChurn(state, amountToWithdraw); const withdrawableEpoch = exitQueueEpoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY; - const pendingPartialWithdrawal = ssz.electra.PartialWithdrawal.toViewDU({ + const pendingPartialWithdrawal = ssz.electra.PendingPartialWithdrawal.toViewDU({ index: validatorIndex, amount: amountToWithdraw, withdrawableEpoch, diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index f996304e9f78..dd6871b8b80a 100644 --- a/packages/state-transition/src/block/processOperations.ts +++ b/packages/state-transition/src/block/processOperations.ts @@ -71,7 +71,7 @@ export function processOperations( processExecutionLayerWithdrawalRequest(fork, state as CachedBeaconStateElectra, elWithdrawalRequest); } - for (const depositRequest of bodyElectra.executionPayload.depositRequests) { + for (const depositRequest of bodyElectra.executionPayload.depositReceipts) { processDepositRequest(fork, stateElectra, depositRequest); } diff --git a/packages/state-transition/src/slot/upgradeStateToElectra.ts b/packages/state-transition/src/slot/upgradeStateToElectra.ts index 121425265b50..417c3cd717c1 100644 --- a/packages/state-transition/src/slot/upgradeStateToElectra.ts +++ b/packages/state-transition/src/slot/upgradeStateToElectra.ts @@ -48,16 +48,16 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache stateElectraView.nextSyncCommittee = stateElectraCloned.nextSyncCommittee; stateElectraView.latestExecutionPayloadHeader = ssz.electra.BeaconState.fields.latestExecutionPayloadHeader.toViewDU({ ...stateElectraCloned.latestExecutionPayloadHeader.toValue(), - depositRequestsRoot: ssz.Root.defaultValue(), + depositReceiptsRoot: ssz.Root.defaultValue(), withdrawalRequestsRoot: ssz.Root.defaultValue(), }); stateElectraView.nextWithdrawalIndex = stateDeneb.nextWithdrawalIndex; stateElectraView.nextWithdrawalValidatorIndex = stateDeneb.nextWithdrawalValidatorIndex; stateElectraView.historicalSummaries = stateElectraCloned.historicalSummaries; - // latestExecutionPayloadHeader's depositRequestsRoot and withdrawalRequestsRoot set to zeros by default - // default value of depositRequestsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX - stateElectraView.depositRequestsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + // latestExecutionPayloadHeader's depositReceiptsRoot and withdrawalRequestsRoot set to zeros by default + // default value of depositReceiptsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX + stateElectraView.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; const validatorsArr = stateElectraView.validators.getAllReadonly(); @@ -99,9 +99,9 @@ export function upgradeStateToElectraOriginal(stateDeneb: CachedBeaconStateDeneb epoch: stateDeneb.epochCtx.epoch, }); - // latestExecutionPayloadHeader's depositRequestsRoot and withdrawalRequestsRoot set to zeros by default - // default value of depositRequestsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX - stateElectra.depositRequestsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + // latestExecutionPayloadHeader's depositReceiptsRoot and withdrawalRequestsRoot set to zeros by default + // default value of depositReceiptsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX + stateElectra.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; const validatorsArr = stateElectra.validators.getAllReadonly(); diff --git a/packages/state-transition/src/util/deposit.ts b/packages/state-transition/src/util/deposit.ts index e8ef93c515d2..493099fdd982 100644 --- a/packages/state-transition/src/util/deposit.ts +++ b/packages/state-transition/src/util/deposit.ts @@ -9,9 +9,9 @@ export function getEth1DepositCount(state: CachedBeaconStateAllForks, eth1Data?: // eth1DataIndexLimit = min(UintNum64, UintBn64) can be safely casted as UintNum64 // since the result lies within upper and lower bound of UintNum64 const eth1DataIndexLimit: UintNum64 = - eth1DataToUse.depositCount < electraState.depositRequestsStartIndex + eth1DataToUse.depositCount < electraState.depositReceiptsStartIndex ? eth1DataToUse.depositCount - : Number(electraState.depositRequestsStartIndex); + : Number(electraState.depositReceiptsStartIndex); if (state.eth1DepositIndex < eth1DataIndexLimit) { return Math.min(MAX_DEPOSITS, eth1DataIndexLimit - state.eth1DepositIndex); diff --git a/packages/state-transition/src/util/execution.ts b/packages/state-transition/src/util/execution.ts index 6939c78416b6..2789836d0e3b 100644 --- a/packages/state-transition/src/util/execution.ts +++ b/packages/state-transition/src/util/execution.ts @@ -171,8 +171,8 @@ export function executionPayloadToPayloadHeader( } if (fork >= ForkSeq.electra) { - (bellatrixPayloadFields as electra.ExecutionPayloadHeader).depositRequestsRoot = - ssz.electra.DepositRequests.hashTreeRoot((payload as electra.ExecutionPayload).depositRequests); + (bellatrixPayloadFields as electra.ExecutionPayloadHeader).depositReceiptsRoot = + ssz.electra.DepositReceipts.hashTreeRoot((payload as electra.ExecutionPayload).depositReceipts); (bellatrixPayloadFields as electra.ExecutionPayloadHeader).withdrawalRequestsRoot = ssz.electra.ExecutionLayerWithdrawalRequests.hashTreeRoot( (payload as electra.ExecutionPayload).withdrawalRequests diff --git a/packages/state-transition/src/util/genesis.ts b/packages/state-transition/src/util/genesis.ts index ca894fd5b4e5..e1003dca1946 100644 --- a/packages/state-transition/src/util/genesis.ts +++ b/packages/state-transition/src/util/genesis.ts @@ -308,7 +308,7 @@ export function initializeBeaconStateFromEth1( stateElectra.latestExecutionPayloadHeader = (executionPayloadHeader as CompositeViewDU) ?? ssz.electra.ExecutionPayloadHeader.defaultViewDU(); - stateElectra.depositRequestsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + stateElectra.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; } state.commit(); diff --git a/packages/state-transition/test/unit/util/deposit.test.ts b/packages/state-transition/test/unit/util/deposit.test.ts index 677a15724ece..e8f7f7a86af8 100644 --- a/packages/state-transition/test/unit/util/deposit.test.ts +++ b/packages/state-transition/test/unit/util/deposit.test.ts @@ -43,7 +43,7 @@ describe("getEth1DepositCount", () => { throw Error("Not a post-Electra state"); } - postElectraState.depositRequestsStartIndex = 1000n; + postElectraState.depositReceiptsStartIndex = 1000n; postElectraState.eth1Data.depositCount = 995; // 1. Should get less than MAX_DEPOSIT @@ -77,7 +77,7 @@ describe("getEth1DepositCount", () => { throw Error("Not a post-Electra state"); } - postElectraState.depositRequestsStartIndex = 1000n; + postElectraState.depositReceiptsStartIndex = 1000n; postElectraState.eth1Data.depositCount = 1005; // Before eth1DepositIndex reaching the start index diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index d3d9843e63f1..b2191215eb2c 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -21,6 +21,8 @@ import { PENDING_BALANCE_DEPOSITS_LIMIT, PENDING_PARTIAL_WITHDRAWALS_LIMIT, PENDING_CONSOLIDATIONS_LIMIT, + FINALIZED_ROOT_DEPTH_ELECTRA, + NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA, } from "@lodestar/params"; import {ssz as primitiveSsz} from "../primitive/index.js"; import {ssz as phase0Ssz} from "../phase0/index.js"; @@ -110,7 +112,7 @@ export const SignedAggregateAndProof = new ContainerType( {typeName: "SignedAggregateAndProof", jsonCase: "eth2"} ); -export const DepositRequest = new ContainerType( +export const DepositReceipt = new ContainerType( { pubkey: BLSPubkey, withdrawalCredentials: Bytes32, @@ -118,10 +120,10 @@ export const DepositRequest = new ContainerType( signature: BLSSignature, index: DepositIndex, }, - {typeName: "DepositRequest", jsonCase: "eth2"} + {typeName: "DepositReceipt", jsonCase: "eth2"} ); -export const DepositRequests = new ListCompositeType(DepositRequest, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD); +export const DepositReceipts = new ListCompositeType(DepositReceipt, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD); export const ExecutionLayerWithdrawalRequest = new ContainerType( { @@ -139,7 +141,7 @@ export const ExecutionLayerWithdrawalRequests = new ListCompositeType( export const ExecutionPayload = new ContainerType( { ...denebSsz.ExecutionPayload.fields, - depositRequests: DepositRequests, // New in ELECTRA + depositReceipts: DepositReceipts, // New in ELECTRA withdrawalRequests: ExecutionLayerWithdrawalRequests, // New in ELECTRA }, {typeName: "ExecutionPayload", jsonCase: "eth2"} @@ -148,7 +150,7 @@ export const ExecutionPayload = new ContainerType( export const ExecutionPayloadHeader = new ContainerType( { ...denebSsz.ExecutionPayloadHeader.fields, - depositRequestsRoot: Root, // New in ELECTRA + depositReceiptsRoot: Root, // New in ELECTRA withdrawalRequestsRoot: Root, // New in ELECTRA }, {typeName: "ExecutionPayloadHeader", jsonCase: "eth2"} @@ -280,13 +282,13 @@ export const PendingBalanceDeposits = new ListCompositeType( Number(PENDING_BALANCE_DEPOSITS_LIMIT) ); -export const PartialWithdrawal = new ContainerType( +export const PendingPartialWithdrawal = new ContainerType( { index: ValidatorIndex, amount: Gwei, withdrawableEpoch: Epoch, }, - {typeName: "PartialWithdrawal", jsonCase: "eth2"} + {typeName: "PendingPartialWithdrawal", jsonCase: "eth2"} ); export const PendingConsolidation = new ContainerType( @@ -340,14 +342,14 @@ export const BeaconState = new ContainerType( nextWithdrawalValidatorIndex: capellaSsz.BeaconState.fields.nextWithdrawalValidatorIndex, // Deep history valid from Capella onwards historicalSummaries: capellaSsz.BeaconState.fields.historicalSummaries, - depositRequestsStartIndex: UintBn64, // New in ELECTRA:EIP6110 + depositReceiptsStartIndex: UintBn64, // New in ELECTRA:EIP6110 depositBalanceToConsume: Gwei, // New in Electra:EIP7251 exitBalanceToConsume: Gwei, // New in Electra:EIP7251 earliestExitEpoch: Epoch, // New in Electra:EIP7251 consolidationBalanceToConsume: Gwei, // New in Electra:EIP7251 earliestConsolidationEpoch: Epoch, // New in Electra:EIP7251 pendingBalanceDeposits: PendingBalanceDeposits, // new in electra:eip7251 - pendingPartialWithdrawals: new ListCompositeType(PartialWithdrawal, Number(PENDING_PARTIAL_WITHDRAWALS_LIMIT)), // New in Electra:EIP7251 + pendingPartialWithdrawals: new ListCompositeType(PendingPartialWithdrawal, Number(PENDING_PARTIAL_WITHDRAWALS_LIMIT)), // New in Electra:EIP7251 pendingConsolidations: new ListCompositeType(PendingConsolidation, Number(PENDING_CONSOLIDATIONS_LIMIT)), // new in electra:eip7251 }, {typeName: "BeaconState", jsonCase: "eth2"} @@ -366,7 +368,7 @@ export const LightClientBootstrap = new ContainerType( { header: LightClientHeader, currentSyncCommittee: altairSsz.SyncCommittee, - currentSyncCommitteeBranch: altairSsz.LightClientBootstrap.fields.currentSyncCommitteeBranch, + currentSyncCommitteeBranch: new VectorCompositeType(Bytes32, NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA), }, {typeName: "LightClientBootstrap", jsonCase: "eth2"} ); @@ -375,9 +377,9 @@ export const LightClientUpdate = new ContainerType( { attestedHeader: LightClientHeader, nextSyncCommittee: altairSsz.SyncCommittee, - nextSyncCommitteeBranch: altairSsz.LightClientUpdate.fields.nextSyncCommitteeBranch, + nextSyncCommitteeBranch: new VectorCompositeType(Bytes32, NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA), finalizedHeader: LightClientHeader, - finalityBranch: altairSsz.LightClientUpdate.fields.finalityBranch, + finalityBranch: new VectorCompositeType(Bytes32, FINALIZED_ROOT_DEPTH_ELECTRA), syncAggregate: altairSsz.SyncAggregate, signatureSlot: Slot, }, @@ -388,7 +390,7 @@ export const LightClientFinalityUpdate = new ContainerType( { attestedHeader: LightClientHeader, finalizedHeader: LightClientHeader, - finalityBranch: altairSsz.LightClientFinalityUpdate.fields.finalityBranch, + finalityBranch: new VectorCompositeType(Bytes32, FINALIZED_ROOT_DEPTH_ELECTRA), syncAggregate: altairSsz.SyncAggregate, signatureSlot: Slot, }, diff --git a/packages/types/src/electra/types.ts b/packages/types/src/electra/types.ts index 53b95e95525c..fc95ecde562d 100644 --- a/packages/types/src/electra/types.ts +++ b/packages/types/src/electra/types.ts @@ -9,8 +9,8 @@ export type AttesterSlashing = ValueOf; export type AggregateAndProof = ValueOf; export type SignedAggregateAndProof = ValueOf; -export type DepositRequest = ValueOf; -export type DepositRequests = ValueOf; +export type DepositRequest = ValueOf; +export type DepositRequests = ValueOf; export type ExecutionLayerWithdrawalRequest = ValueOf; export type ExecutionLayerWithdrawalRequests = ValueOf; @@ -47,5 +47,5 @@ export type Consolidation = ValueOf; export type SignedConsolidation = ValueOf; export type PendingBalanceDeposit = ValueOf; -export type PartialWithdrawal = ValueOf; +export type PendingPartialWithdrawal = ValueOf; export type PendingConsolidation = ValueOf; From 0707a6025db52203d7fad17e2757d32c48bc2400 Mon Sep 17 00:00:00 2001 From: Matthew Keil Date: Tue, 14 May 2024 04:42:06 -0400 Subject: [PATCH 28/51] fix: inline sourcemaps to help with debugging (#6768) --- vite.base.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vite.base.config.ts b/vite.base.config.ts index 65e1bad01500..f9030d9edb7f 100644 --- a/vite.base.config.ts +++ b/vite.base.config.ts @@ -40,6 +40,7 @@ export function getBaseViteConfig( esbuild: { banner, legalComments: "none", + sourcemap: "inline", }, build: { // "modules" refer to ['es2020', 'edge88', 'firefox78', 'chrome87', 'safari14'] From 4b8a3155dd0f25f9bbfc574c129b593b846dc4cf Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Tue, 14 May 2024 11:46:01 +0200 Subject: [PATCH 29/51] fix: additional epoch calculation logic for consolidation churn (#6770) Fix the chunk limit logic --- packages/state-transition/src/util/epoch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/state-transition/src/util/epoch.ts b/packages/state-transition/src/util/epoch.ts index ca56b711e0be..e844a191cf6f 100644 --- a/packages/state-transition/src/util/epoch.ts +++ b/packages/state-transition/src/util/epoch.ts @@ -83,7 +83,7 @@ export function computeConsolidationEpochAndUpdateChurn( // Consolidation doesn't fit in the current earliest epoch. if (consolidationBalance > consolidationBalanceToConsume) { const balanceToProcess = Number(consolidationBalance) - consolidationBalanceToConsume; - const additionalEpochs = Math.floor((balanceToProcess - 1) / (perEpochConsolidationChurn + 1)); + const additionalEpochs = Math.floor((balanceToProcess - 1) / perEpochConsolidationChurn) + 1; earliestConsolidationEpoch += additionalEpochs; consolidationBalanceToConsume += additionalEpochs * perEpochConsolidationChurn; } From 4f99b2e6650ed486f5012cb59dbf591190295c33 Mon Sep 17 00:00:00 2001 From: twoeths Date: Tue, 14 May 2024 12:56:01 +0300 Subject: [PATCH 30/51] fix: electra fork transition spec tests (#6769) * fix: electra fork transition * fix: merge issue * chore: remove unwanted change --- .../src/slot/upgradeStateToElectra.ts | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/packages/state-transition/src/slot/upgradeStateToElectra.ts b/packages/state-transition/src/slot/upgradeStateToElectra.ts index 417c3cd717c1..72255ccddee3 100644 --- a/packages/state-transition/src/slot/upgradeStateToElectra.ts +++ b/packages/state-transition/src/slot/upgradeStateToElectra.ts @@ -1,4 +1,4 @@ -import {ssz} from "@lodestar/types"; +import {Epoch, ValidatorIndex, ssz} from "@lodestar/types"; import {FAR_FUTURE_EPOCH, UNSET_DEPOSIT_RECEIPTS_START_INDEX} from "@lodestar/params"; import {CachedBeaconStateDeneb} from "../types.js"; import {CachedBeaconStateElectra, getCachedBeaconState} from "../cache/stateCache.js"; @@ -7,6 +7,8 @@ import { queueEntireBalanceAndResetValidator, queueExcessActiveBalance, } from "../util/electra.js"; +import {computeActivationExitEpoch} from "../util/epoch.js"; +import {getActivationExitChurnLimit, getConsolidationChurnLimit} from "../util/validator.js"; /** * Upgrade a state from Capella to Deneb. @@ -58,17 +60,52 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache // latestExecutionPayloadHeader's depositReceiptsRoot and withdrawalRequestsRoot set to zeros by default // default value of depositReceiptsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX stateElectraView.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + stateElectraView.depositBalanceToConsume = BigInt(0); + stateElectraView.exitBalanceToConsume = BigInt(0); const validatorsArr = stateElectraView.validators.getAllReadonly(); + const exitEpochs: Epoch[] = []; + + // [EIP-7251]: add validators that are not yet active to pending balance deposits + const preActivation: ValidatorIndex[] = []; + for (let validatorIndex = 0; validatorIndex < validatorsArr.length; validatorIndex++) { + const {activationEpoch, exitEpoch} = validatorsArr[validatorIndex]; + if (activationEpoch === FAR_FUTURE_EPOCH) { + preActivation.push(validatorIndex); + } + if (exitEpoch !== FAR_FUTURE_EPOCH) { + exitEpochs.push(exitEpoch); + } + } + + const currentEpochPre = stateDeneb.epochCtx.epoch; + + if (exitEpochs.length === 0) { + exitEpochs.push(currentEpochPre); + } + stateElectraView.earliestExitEpoch = Math.max(...exitEpochs) + 1; + stateElectraView.consolidationBalanceToConsume = BigInt(0); + stateElectraView.earliestConsolidationEpoch = computeActivationExitEpoch(currentEpochPre); + // stateElectraView.pendingBalanceDeposits = ssz.electra.PendingBalanceDeposits.defaultViewDU(); + // pendingBalanceDeposits, pendingPartialWithdrawals, pendingConsolidations are default values + // TODO-electra: can we improve this? + stateElectraView.commit(); + const tmpElectraState = getCachedBeaconState(stateElectraView, stateDeneb); + stateElectraView.exitBalanceToConsume = BigInt(getActivationExitChurnLimit(tmpElectraState)); + stateElectraView.consolidationBalanceToConsume = BigInt(getConsolidationChurnLimit(tmpElectraState)); + + preActivation.sort((i0, i1) => { + const res = validatorsArr[i0].activationEligibilityEpoch - validatorsArr[i1].activationEligibilityEpoch; + return res !== 0 ? res : i0 - i1; + }); + + for (const validatorIndex of preActivation) { + queueEntireBalanceAndResetValidator(stateElectraView as CachedBeaconStateElectra, validatorIndex); + } for (let i = 0; i < validatorsArr.length; i++) { const validator = validatorsArr[i]; - // [EIP-7251]: add validators that are not yet active to pending balance deposits - if (validator.activationEligibilityEpoch === FAR_FUTURE_EPOCH) { - queueEntireBalanceAndResetValidator(stateElectraView as CachedBeaconStateElectra, i); - } - // [EIP-7251]: Ensure early adopters of compounding credentials go through the activation churn const withdrawalCredential = validator.withdrawalCredentials; if (hasCompoundingWithdrawalCredential(withdrawalCredential)) { From 5b80b000f11a7bf8fc9c6e7b4e76d1cd57dbfee5 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 14 May 2024 14:50:01 +0300 Subject: [PATCH 31/51] test: fix ssz_static spec tests for all forks (#6771) --- .../beacon-node/test/spec/presets/operations.test.ts | 7 ++++--- .../beacon-node/test/spec/presets/ssz_static.test.ts | 4 +++- packages/params/src/index.ts | 2 +- packages/types/src/allForks/sszTypes.ts | 12 ++++++++++++ packages/types/src/electra/sszTypes.ts | 11 ++++------- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index 228f6fd033d7..c6d5d8297673 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -138,12 +138,12 @@ const operations: TestRunnerFn = (fork, sszTypes: { pre: ssz[fork].BeaconState, post: ssz[fork].BeaconState, - attestation: fork === ForkName.electra ? ssz.electra.Attestation : ssz.phase0.Attestation, - attester_slashing: fork === ForkName.electra ? ssz.electra.AttesterSlashing : ssz.phase0.AttesterSlashing, + attestation: ssz.allForks[fork].Attestation, + attester_slashing: ssz.allForks[fork].AttesterSlashing, block: ssz[fork].BeaconBlock, body: ssz[fork].BeaconBlockBody, deposit: ssz.phase0.Deposit, - deposit_receipt: ssz.electra.DepositRequest, + deposit_receipt: ssz.electra.DepositReceipt, proposer_slashing: ssz.phase0.ProposerSlashing, voluntary_exit: ssz.phase0.SignedVoluntaryExit, // Altair @@ -155,6 +155,7 @@ const operations: TestRunnerFn = (fork, : ssz.bellatrix.ExecutionPayload, // Capella address_change: ssz.capella.SignedBLSToExecutionChange, + // Electra consolidation: ssz.electra.SignedConsolidation, execution_layer_withdrawal_request: ssz.electra.ExecutionLayerWithdrawalRequest, }, diff --git a/packages/beacon-node/test/spec/presets/ssz_static.test.ts b/packages/beacon-node/test/spec/presets/ssz_static.test.ts index 468ad7ab7144..47808f76ccae 100644 --- a/packages/beacon-node/test/spec/presets/ssz_static.test.ts +++ b/packages/beacon-node/test/spec/presets/ssz_static.test.ts @@ -3,7 +3,7 @@ import path from "node:path"; import {it, vi} from "vitest"; import {Type} from "@chainsafe/ssz"; import {ssz} from "@lodestar/types"; -import {ACTIVE_PRESET, ForkName, ForkLightClient} from "@lodestar/params"; +import {ACTIVE_PRESET, ForkName, ForkLightClient, ForkExecution} from "@lodestar/params"; import {replaceUintTypeWithUintBigintType} from "../utils/replaceUintTypeWithUintBigintType.js"; import {parseSszStaticTestcase} from "../utils/sszTestCaseParser.js"; import {runValidSszTest} from "../utils/runValidSszTest.js"; @@ -47,6 +47,8 @@ const sszStatic = // Since lightclient types are not updated/declared at all forks, this allForksLightClient // will help us get the right type for lightclient objects ((ssz.allForksLightClient[fork as ForkLightClient] || {}) as Types)[typeName] || + ((ssz.allForksExecution[fork as ForkExecution] || {}) as Types)[typeName] || + (ssz.allForks[fork] as Types)[typeName] || (ssz[fork] as Types)[typeName] || (ssz.electra as Types)[typeName] || (ssz.deneb as Types)[typeName] || diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index 20439568156e..0c2e3b34794b 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -267,4 +267,4 @@ export const FULL_EXIT_REQUEST_AMOUNT = 0; export const NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA = 87; export const NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA = 6; export const FINALIZED_ROOT_DEPTH_ELECTRA = 7; -export const FINALIZED_ROOT_INDEX_ELECTRA = 169; \ No newline at end of file +export const FINALIZED_ROOT_INDEX_ELECTRA = 169; diff --git a/packages/types/src/allForks/sszTypes.ts b/packages/types/src/allForks/sszTypes.ts index 0ad3b18b2f8e..b822bd4af40c 100644 --- a/packages/types/src/allForks/sszTypes.ts +++ b/packages/types/src/allForks/sszTypes.ts @@ -16,8 +16,10 @@ export const allForks = { SignedBeaconBlock: phase0.SignedBeaconBlock, BeaconState: phase0.BeaconState, Metadata: phase0.Metadata, + AggregateAndProof: phase0.AggregateAndProof, SignedAggregateAndProof: phase0.SignedAggregateAndProof, Attestation: phase0.Attestation, + IndexedAttestation: phase0.IndexedAttestation, AttesterSlashing: phase0.AttesterSlashing, }, altair: { @@ -26,8 +28,10 @@ export const allForks = { SignedBeaconBlock: altair.SignedBeaconBlock, BeaconState: altair.BeaconState, Metadata: altair.Metadata, + AggregateAndProof: phase0.AggregateAndProof, SignedAggregateAndProof: phase0.SignedAggregateAndProof, Attestation: phase0.Attestation, + IndexedAttestation: phase0.IndexedAttestation, AttesterSlashing: phase0.AttesterSlashing, }, bellatrix: { @@ -36,8 +40,10 @@ export const allForks = { SignedBeaconBlock: bellatrix.SignedBeaconBlock, BeaconState: bellatrix.BeaconState, Metadata: altair.Metadata, + AggregateAndProof: phase0.AggregateAndProof, SignedAggregateAndProof: phase0.SignedAggregateAndProof, Attestation: phase0.Attestation, + IndexedAttestation: phase0.IndexedAttestation, AttesterSlashing: phase0.AttesterSlashing, }, capella: { @@ -46,8 +52,10 @@ export const allForks = { SignedBeaconBlock: capella.SignedBeaconBlock, BeaconState: capella.BeaconState, Metadata: altair.Metadata, + AggregateAndProof: phase0.AggregateAndProof, SignedAggregateAndProof: phase0.SignedAggregateAndProof, Attestation: phase0.Attestation, + IndexedAttestation: phase0.IndexedAttestation, AttesterSlashing: phase0.AttesterSlashing, }, deneb: { @@ -56,8 +64,10 @@ export const allForks = { SignedBeaconBlock: deneb.SignedBeaconBlock, BeaconState: deneb.BeaconState, Metadata: altair.Metadata, + AggregateAndProof: phase0.AggregateAndProof, SignedAggregateAndProof: phase0.SignedAggregateAndProof, Attestation: phase0.Attestation, + IndexedAttestation: phase0.IndexedAttestation, AttesterSlashing: phase0.AttesterSlashing, }, electra: { @@ -66,8 +76,10 @@ export const allForks = { SignedBeaconBlock: electra.SignedBeaconBlock, BeaconState: electra.BeaconState, Metadata: altair.Metadata, + AggregateAndProof: electra.AggregateAndProof, SignedAggregateAndProof: electra.SignedAggregateAndProof, Attestation: electra.Attestation, + IndexedAttestation: electra.IndexedAttestation, AttesterSlashing: electra.AttesterSlashing, }, }; diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index b2191215eb2c..0aa2872c3a73 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -277,10 +277,7 @@ export const PendingBalanceDeposit = new ContainerType( {typeName: "PendingBalanceDeposit", jsonCase: "eth2"} ); -export const PendingBalanceDeposits = new ListCompositeType( - PendingBalanceDeposit, - Number(PENDING_BALANCE_DEPOSITS_LIMIT) -); +export const PendingBalanceDeposits = new ListCompositeType(PendingBalanceDeposit, PENDING_BALANCE_DEPOSITS_LIMIT); export const PendingPartialWithdrawal = new ContainerType( { @@ -348,9 +345,9 @@ export const BeaconState = new ContainerType( earliestExitEpoch: Epoch, // New in Electra:EIP7251 consolidationBalanceToConsume: Gwei, // New in Electra:EIP7251 earliestConsolidationEpoch: Epoch, // New in Electra:EIP7251 - pendingBalanceDeposits: PendingBalanceDeposits, // new in electra:eip7251 - pendingPartialWithdrawals: new ListCompositeType(PendingPartialWithdrawal, Number(PENDING_PARTIAL_WITHDRAWALS_LIMIT)), // New in Electra:EIP7251 - pendingConsolidations: new ListCompositeType(PendingConsolidation, Number(PENDING_CONSOLIDATIONS_LIMIT)), // new in electra:eip7251 + pendingBalanceDeposits: PendingBalanceDeposits, // New in Electra:EIP7251 + pendingPartialWithdrawals: new ListCompositeType(PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT), // New in Electra:EIP7251 + pendingConsolidations: new ListCompositeType(PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT), // New in Electra:EIP7251 }, {typeName: "BeaconState", jsonCase: "eth2"} ); From f814364c3e5e4df24c146a163f3a5b609710d2ae Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Tue, 14 May 2024 14:11:00 +0200 Subject: [PATCH 32/51] chore(spec): remove the skip specs for electra (#6772) Remove the skip spec for electra --- packages/beacon-node/test/spec/utils/specTestIterator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 393983bb8a56..63fe0cc10442 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -65,7 +65,6 @@ export const defaultSkipOpts: SkipOpts = { skippedTestSuites: [ /^capella\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, /^deneb\/light_client\/single_merkle_proof\/BeaconBlockBody.*/, - /^electra\/(?!operations\/attestations)(?!operations\/attester_slashing)/, ], skippedTests: [], skippedRunners: ["merkle_proof", "networking"], From d1b5769559930fe4a564897e0189cae9679e6342 Mon Sep 17 00:00:00 2001 From: NC Date: Tue, 14 May 2024 15:26:36 +0300 Subject: [PATCH 33/51] fix: use mutable validator object (#6774) Use mutable validator object --- .../src/block/processExecutionLayerWithdrawalRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts b/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts index 1b795789cb77..e02129a09d05 100644 --- a/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts +++ b/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts @@ -37,7 +37,7 @@ export function processExecutionLayerWithdrawalRequest( return; } - const validator = validators.getReadonly(validatorIndex); + const validator = validators.get(validatorIndex); if (!isValidatorEligibleForWithdrawOrExit(validator, executionLayerWithdrawalRequest.sourceAddress, state)) { return; } From baeb740d6d432d3561384bc2893aa0cb46b6e6f5 Mon Sep 17 00:00:00 2001 From: Matthew Keil Date: Tue, 14 May 2024 11:42:28 -0400 Subject: [PATCH 34/51] test: fix balance spec tests (#6777) * fix: remove epochCache.balances and invalid MAX_EFFECTIVE_BALANCE check * fix: update rewardsAndPenalties balance updates * docs: add comment to check epochTransitionCache --- .../src/epoch/processEffectiveBalanceUpdates.ts | 7 +++++-- .../src/epoch/processRewardsAndPenalties.ts | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts b/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts index e83d2545ef50..f4fae6e41d4f 100644 --- a/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts +++ b/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts @@ -41,7 +41,10 @@ export function processEffectiveBalanceUpdates( // epochTransitionCache.balances is set in processRewardsAndPenalties(), so it's recycled here for performance. // It defaults to `state.balances.getAll()` to make Typescript happy and for spec tests - const balances = cache.balances ?? state.balances.getAll(); + const balances = state.balances.getAll(); + + // TODO: (@matthewkeil) This was causing additional failures but should not. Check the EpochTransitionCache for why + // const balances = cache.balances ?? state.balances.getAll(); for (let i = 0, len = balances.length; i < len; i++) { const balance = balances[i]; @@ -55,7 +58,7 @@ export function processEffectiveBalanceUpdates( // Too big effectiveBalance > balance + DOWNWARD_THRESHOLD || // Too small. Check effectiveBalance < MAX_EFFECTIVE_BALANCE to prevent unnecessary updates - (effectiveBalance < MAX_EFFECTIVE_BALANCE && effectiveBalance < balance - UPWARD_THRESHOLD) + effectiveBalance + UPWARD_THRESHOLD < balance ) { // Update the state tree // Should happen rarely, so it's fine to update the tree diff --git a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts index 61680b81002a..041655d75825 100644 --- a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts +++ b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts @@ -28,7 +28,8 @@ export function processRewardsAndPenalties( const balances = state.balances.getAll(); for (let i = 0, len = rewards.length; i < len; i++) { - balances[i] += rewards[i] - penalties[i] - (slashingPenalties[i] ?? 0); + const result = balances[i] + rewards[i] - penalties[i] - (slashingPenalties[i] ?? 0) + balances[i] = Math.max(result, 0); } // important: do not change state one balance at a time. Set them all at once, constructing the tree in one go From 8d18fbdfdbfdf256e39de88064b83a92e286e59c Mon Sep 17 00:00:00 2001 From: NC Date: Tue, 14 May 2024 22:15:17 +0300 Subject: [PATCH 35/51] fix: effective balance cache is not in sync with validator effective balance (#6780) Update eb cache at fork transition --- packages/state-transition/src/util/electra.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/state-transition/src/util/electra.ts b/packages/state-transition/src/util/electra.ts index 00c5bb183eb4..53f53aaf8d61 100644 --- a/packages/state-transition/src/util/electra.ts +++ b/packages/state-transition/src/util/electra.ts @@ -93,6 +93,7 @@ export function queueEntireBalanceAndResetValidator(state: CachedBeaconStateElec const validator = state.validators.get(index); validator.effectiveBalance = 0; + state.epochCtx.effectiveBalanceIncrementsSet(index, 0); validator.activationEligibilityEpoch = FAR_FUTURE_EPOCH; const pendingBalanceDeposit = ssz.electra.PendingBalanceDeposit.toViewDU({ From 06680764a66170fbdba14fec01911c4a63cdea4b Mon Sep 17 00:00:00 2001 From: NC Date: Thu, 16 May 2024 11:05:01 +0300 Subject: [PATCH 36/51] fix: make electra-fork passes lint and check-types (#6785) fix lint and check-types --- .../src/chain/validation/signatureSets/aggregateAndProof.ts | 2 +- packages/beacon-node/test/sim/electra-interop.test.ts | 6 +++--- packages/fork-choice/test/perf/forkChoice/util.ts | 2 +- .../fork-choice/test/unit/forkChoice/forkChoice.test.ts | 6 +++--- .../test/unit/forkChoice/getProposerHead.test.ts | 6 +++--- .../fork-choice/test/unit/protoArray/computeDeltas.test.ts | 2 +- .../src/epoch/processRewardsAndPenalties.ts | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts index 1c18723feac0..411f33f68e66 100644 --- a/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts @@ -1,7 +1,7 @@ import type {PublicKey} from "@chainsafe/bls/types"; import {DOMAIN_AGGREGATE_AND_PROOF, ForkSeq} from "@lodestar/params"; import {allForks, ssz} from "@lodestar/types"; -import {Epoch, phase0} from "@lodestar/types"; +import {Epoch} from "@lodestar/types"; import { computeSigningRoot, computeStartSlotAtEpoch, diff --git a/packages/beacon-node/test/sim/electra-interop.test.ts b/packages/beacon-node/test/sim/electra-interop.test.ts index 37bed1f7b843..962e0aa5dea9 100644 --- a/packages/beacon-node/test/sim/electra-interop.test.ts +++ b/packages/beacon-node/test/sim/electra-interop.test.ts @@ -219,11 +219,11 @@ describe("executionEngine / ExecutionEngineHttp", function () { } } - if (payload.depositRequests.length !== 1) { - throw Error(`Number of depositRequests mismatched. Expected: 1, actual: ${payload.depositRequests.length}`); + if (payload.depositReceipts.length !== 1) { + throw Error(`Number of depositRequests mismatched. Expected: 1, actual: ${payload.depositReceipts.length}`); } - const actualDepositRequest = payload.depositRequests[0]; + const actualDepositRequest = payload.depositReceipts[0]; assert.deepStrictEqual( actualDepositRequest, depositRequestB, diff --git a/packages/fork-choice/test/perf/forkChoice/util.ts b/packages/fork-choice/test/perf/forkChoice/util.ts index dbd049e257c1..6c04ac817fb2 100644 --- a/packages/fork-choice/test/perf/forkChoice/util.ts +++ b/packages/fork-choice/test/perf/forkChoice/util.ts @@ -41,7 +41,7 @@ export function initializeForkChoice(opts: Opts): ForkChoice { genesisSlot ); - const balances = new Uint8Array(Array.from({length: opts.initialValidatorCount}, () => 32)); + const balances = new Uint16Array(Array.from({length: opts.initialValidatorCount}, () => 32)); const fcStore: IForkChoiceStore = { currentSlot: genesisSlot, diff --git a/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts b/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts index fcb7376cffa8..40988a0e4a71 100644 --- a/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts +++ b/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts @@ -52,16 +52,16 @@ describe("Forkchoice", function () { currentSlot: genesisSlot + 1, justified: { checkpoint: {epoch: genesisEpoch, root: fromHexString(finalizedRoot), rootHex: finalizedRoot}, - balances: new Uint8Array([32]), + balances: new Uint16Array([32]), totalBalance: 32, }, unrealizedJustified: { checkpoint: {epoch: genesisEpoch, root: fromHexString(finalizedRoot), rootHex: finalizedRoot}, - balances: new Uint8Array([32]), + balances: new Uint16Array([32]), }, finalizedCheckpoint: {epoch: genesisEpoch, root: fromHexString(finalizedRoot), rootHex: finalizedRoot}, unrealizedFinalizedCheckpoint: {epoch: genesisEpoch, root: fromHexString(finalizedRoot), rootHex: finalizedRoot}, - justifiedBalancesGetter: () => new Uint8Array([32]), + justifiedBalancesGetter: () => new Uint16Array([32]), equivocatingIndices: new Set(), }; diff --git a/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts b/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts index cc14b5b57b92..f603a4069b83 100644 --- a/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts +++ b/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts @@ -102,12 +102,12 @@ describe("Forkchoice / GetProposerHead", function () { currentSlot: genesisSlot + 1, justified: { checkpoint: {epoch: genesisEpoch, root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot}, - balances: new Uint8Array(Array(32).fill(150)), + balances: new Uint16Array(Array(32).fill(150)), totalBalance: 32 * 150, }, unrealizedJustified: { checkpoint: {epoch: genesisEpoch, root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot}, - balances: new Uint8Array(Array(32).fill(150)), + balances: new Uint16Array(Array(32).fill(150)), }, finalizedCheckpoint: { epoch: genesisEpoch, @@ -119,7 +119,7 @@ describe("Forkchoice / GetProposerHead", function () { root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot, }, - justifiedBalancesGetter: () => new Uint8Array(Array(32).fill(150)), + justifiedBalancesGetter: () => new Uint16Array(Array(32).fill(150)), equivocatingIndices: new Set(), }; diff --git a/packages/fork-choice/test/unit/protoArray/computeDeltas.test.ts b/packages/fork-choice/test/unit/protoArray/computeDeltas.test.ts index fde551d43cda..4428807bd13d 100644 --- a/packages/fork-choice/test/unit/protoArray/computeDeltas.test.ts +++ b/packages/fork-choice/test/unit/protoArray/computeDeltas.test.ts @@ -253,7 +253,7 @@ describe("computeDeltas", () => { nextEpoch: 0, })); - const balances = new Uint8Array([firstBalance, secondBalance]); + const balances = new Uint16Array([firstBalance, secondBalance]); // 1st validator is part of an attester slashing const equivocatingIndices = new Set([0]); let deltas = computeDeltas(indices.size, votes, balances, balances, equivocatingIndices); diff --git a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts index 041655d75825..6c5d5aa3cb5a 100644 --- a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts +++ b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts @@ -28,7 +28,7 @@ export function processRewardsAndPenalties( const balances = state.balances.getAll(); for (let i = 0, len = rewards.length; i < len; i++) { - const result = balances[i] + rewards[i] - penalties[i] - (slashingPenalties[i] ?? 0) + const result = balances[i] + rewards[i] - penalties[i] - (slashingPenalties[i] ?? 0); balances[i] = Math.max(result, 0); } From f0dc964545a958e11567ba5640544798cde85006 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 16 May 2024 11:54:51 +0300 Subject: [PATCH 37/51] fix: update data format of WithdrawalRequestV1 (#6789) --- packages/beacon-node/src/execution/engine/payloadIdCache.ts | 2 +- packages/beacon-node/src/execution/engine/types.ts | 4 ++-- .../src/block/processExecutionLayerWithdrawalRequest.ts | 2 +- packages/types/src/electra/sszTypes.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/beacon-node/src/execution/engine/payloadIdCache.ts b/packages/beacon-node/src/execution/engine/payloadIdCache.ts index f79c28582459..35a50e97cfb3 100644 --- a/packages/beacon-node/src/execution/engine/payloadIdCache.ts +++ b/packages/beacon-node/src/execution/engine/payloadIdCache.ts @@ -28,7 +28,7 @@ export type DepositRequestV1 = { export type ExecutionLayerWithdrawalRequestV1 = { sourceAddress: DATA; - validatorPubkey: DATA; + validatorPublicKey: DATA; amount: QUANTITY; }; diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index 43c269d1c402..f957722c0955 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -419,7 +419,7 @@ export function serializeExecutionLayerWithdrawalRequest( ): ExecutionLayerWithdrawalRequestRpc { return { sourceAddress: bytesToData(withdrawalRequest.sourceAddress), - validatorPubkey: bytesToData(withdrawalRequest.validatorPubkey), + validatorPublicKey: bytesToData(withdrawalRequest.validatorPublicKey), amount: numToQuantity(withdrawalRequest.amount), }; } @@ -429,7 +429,7 @@ export function deserializeExecutionLayerWithdrawalRequest( ): electra.ExecutionLayerWithdrawalRequest { return { sourceAddress: dataToBytes(withdrawalRequest.sourceAddress, 20), - validatorPubkey: dataToBytes(withdrawalRequest.validatorPubkey, 48), + validatorPublicKey: dataToBytes(withdrawalRequest.validatorPublicKey, 48), amount: quantityToNum(withdrawalRequest.amount), }; } diff --git a/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts b/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts index e02129a09d05..935acd522da1 100644 --- a/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts +++ b/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts @@ -32,7 +32,7 @@ export function processExecutionLayerWithdrawalRequest( // bail out if validator is not in beacon state // note that we don't need to check for 6110 unfinalized vals as they won't be eligible for withdraw/exit anyway - const validatorIndex = pubkey2index.get(executionLayerWithdrawalRequest.validatorPubkey); + const validatorIndex = pubkey2index.get(executionLayerWithdrawalRequest.validatorPublicKey); if (validatorIndex === undefined) { return; } diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index 0aa2872c3a73..3596bdbce5bf 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -128,7 +128,7 @@ export const DepositReceipts = new ListCompositeType(DepositReceipt, MAX_DEPOSIT export const ExecutionLayerWithdrawalRequest = new ContainerType( { sourceAddress: ExecutionAddress, - validatorPubkey: BLSPubkey, + validatorPublicKey: BLSPubkey, amount: UintNum64, }, {typeName: "ExecutionLayerWithdrawalRequest", jsonCase: "eth2"} From ef4092cb4f5bfecb19649028eacc2be41abd7052 Mon Sep 17 00:00:00 2001 From: NC Date: Thu, 16 May 2024 12:53:29 +0300 Subject: [PATCH 38/51] fix: publish attestations with non-zero committee index (#6790) Fix publishing att with non-zero comm index --- packages/validator/src/services/attestation.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/validator/src/services/attestation.ts b/packages/validator/src/services/attestation.ts index 8f5228e3a10e..7be2c74cd21d 100644 --- a/packages/validator/src/services/attestation.ts +++ b/packages/validator/src/services/attestation.ts @@ -148,12 +148,13 @@ export class AttestationService { this.metrics?.attesterStepCallProduceAggregate.observe(this.clock.secFromSlot(slot + 2 / 3)); const dutiesByCommitteeIndex = groupAttDutiesByCommitteeIndex(dutiesAll); + const isAfterElectra = computeEpochAtSlot(slot) >= this.config.ELECTRA_FORK_EPOCH; // Then download, sign and publish a `SignedAggregateAndProof` for each // validator that is elected to aggregate for this `slot` and `committeeIndex`. await Promise.all( Array.from(dutiesByCommitteeIndex.entries()).map(([index, dutiesSameCommittee]) => { - const attestationData: phase0.AttestationData = {...attestationNoCommittee, index}; + const attestationData: phase0.AttestationData = {...attestationNoCommittee, index: isAfterElectra ? 0 : index}; return this.produceAndPublishAggregates(attestationData, index, dutiesSameCommittee); }) ); @@ -184,10 +185,11 @@ export class AttestationService { const signedAttestations: phase0.Attestation[] = []; const headRootHex = toHexString(attestationNoCommittee.beaconBlockRoot); const currentEpoch = computeEpochAtSlot(slot); + const isAfterElectra = currentEpoch >= this.config.ELECTRA_FORK_EPOCH; await Promise.all( duties.map(async ({duty}) => { - const index = duty.committeeIndex; + const index = isAfterElectra ? 0 : duty.committeeIndex; const attestationData: phase0.AttestationData = {...attestationNoCommittee, index}; const logCtxValidator = {slot, index, head: headRootHex, validatorIndex: duty.validatorIndex}; From cb6b7068644cc51acbd8a3a897b410d3e8a917df Mon Sep 17 00:00:00 2001 From: NC Date: Thu, 16 May 2024 15:08:22 +0300 Subject: [PATCH 39/51] fix: validator monitor summaries should not render during epoch 0 (#6791) Skip render summary at epoch 0 --- packages/beacon-node/src/metrics/validatorMonitor.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/src/metrics/validatorMonitor.ts b/packages/beacon-node/src/metrics/validatorMonitor.ts index 11fe3b17a357..ac6ae776536c 100644 --- a/packages/beacon-node/src/metrics/validatorMonitor.ts +++ b/packages/beacon-node/src/metrics/validatorMonitor.ts @@ -633,7 +633,14 @@ export function createValidatorMonitor( } // Compute summaries of previous epoch attestation performance - const prevEpoch = Math.max(0, computeEpochAtSlot(headState.slot) - 1); + const prevEpoch = computeEpochAtSlot(headState.slot) - 1; + + // During the end of first epoch, the prev epoch with be -1 + // Skip this as there is no attestation and block proposal summary in epoch -1 + if (prevEpoch === -1) { + return; + } + const rootCache = new RootHexCache(headState); if (config.getForkSeq(headState.slot) >= ForkSeq.altair) { From 81b993a304edc45efe18b228cffb65ec620592b1 Mon Sep 17 00:00:00 2001 From: NC Date: Thu, 16 May 2024 15:11:37 +0300 Subject: [PATCH 40/51] fix: attestation duty validation (#6792) * fix attestation duty validation * Update packages/validator/src/services/validatorStore.ts Co-authored-by: twoeths * Update packages/validator/src/services/validatorStore.ts --------- Co-authored-by: twoeths Co-authored-by: Cayman --- packages/validator/src/services/validatorStore.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index d7a945998f33..04923cbddf30 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -795,11 +795,18 @@ export class ValidatorStore { if (duty.slot !== data.slot) { throw Error(`Inconsistent duties during signing: duty.slot ${duty.slot} != att.slot ${data.slot}`); } - if (duty.committeeIndex != data.index) { + + const isAfterElectra = computeEpochAtSlot(duty.slot) >= this.config.ELECTRA_FORK_EPOCH; + if (!isAfterElectra && duty.committeeIndex != data.index) { throw Error( `Inconsistent duties during signing: duty.committeeIndex ${duty.committeeIndex} != att.committeeIndex ${data.index}` ); } + if (isAfterElectra && data.index !== 0) { + throw Error( + `Non-zero committee index post-electra during signing: att.committeeIndex ${data.index}` + ); + } if (this.config.getForkSeq(duty.slot) >= ForkSeq.electra && data.index !== 0) { throw Error(`Attestataion data index must be 0 post electra: index ${data.index}`); } From e78ee555fb467a20efa5cf78916d3f1e9eee11ad Mon Sep 17 00:00:00 2001 From: Cayman Date: Thu, 16 May 2024 08:12:40 -0400 Subject: [PATCH 41/51] fix: align BeaconBlockBody and BlindedBeaconBlockBody (#6782) * fix: align BeaconBlockBody and BlindedBeaconBlockBody * Remove type hacks in test --------- Co-authored-by: Nico Flaig --- packages/types/src/electra/sszTypes.ts | 1 + packages/types/test/unit/blinded.test.ts | 35 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 packages/types/test/unit/blinded.test.ts diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index 3596bdbce5bf..0a0c0f6aeefb 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -223,6 +223,7 @@ export const BlindedBeaconBlockBody = new ContainerType( executionPayloadHeader: ExecutionPayloadHeader, // Modified in ELECTRA blsToExecutionChanges: capellaSsz.BeaconBlockBody.fields.blsToExecutionChanges, blobKzgCommitments: denebSsz.BeaconBlockBody.fields.blobKzgCommitments, + consolidations: new ListCompositeType(SignedConsolidation, MAX_CONSOLIDATIONS), // [New in Electra] }, {typeName: "BlindedBeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} ); diff --git a/packages/types/test/unit/blinded.test.ts b/packages/types/test/unit/blinded.test.ts new file mode 100644 index 000000000000..77005f1defc2 --- /dev/null +++ b/packages/types/test/unit/blinded.test.ts @@ -0,0 +1,35 @@ +import {describe, it, expect} from "vitest"; +import {ForkName, isForkExecution} from "@lodestar/params"; +import {ssz} from "../../src/index.js"; + +describe("blinded data structures", function () { + it("should have the same number of fields as non-blinded", () => { + const blindedTypes = [ + {a: "BlindedBeaconBlockBody" as const, b: "BeaconBlockBody" as const}, + {a: "ExecutionPayloadHeader" as const, b: "ExecutionPayload" as const}, + ]; + + for (const {a, b} of blindedTypes) { + for (const fork of Object.keys(ssz.allForks) as ForkName[]) { + if (!isForkExecution(fork)) { + continue; + } + + const blindedType = ssz[fork][a]; + if (blindedType === undefined) { + expect.fail(`fork: ${fork}, type ${a} is undefined`); + } + + const type = ssz[fork][b]; + if (type === undefined) { + expect.fail(`fork: ${fork}, type ${b} is undefined`); + } + + expect(Object.keys(blindedType.fields).length).toBeWithMessage( + Object.keys(type.fields).length, + `fork: ${fork}, types ${a} and ${b} have different number of fields` + ); + } + } + }); +}); From fdefed8f8924ceffafd0e44c1cfa23255dbc2fb3 Mon Sep 17 00:00:00 2001 From: Julien Date: Thu, 16 May 2024 05:17:49 -0700 Subject: [PATCH 42/51] test: improve ssz tests consistency (#6776) * test: improve ssz tests consistency * chore: address comments --- .../test/spec/presets/ssz_static.test.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/beacon-node/test/spec/presets/ssz_static.test.ts b/packages/beacon-node/test/spec/presets/ssz_static.test.ts index 47808f76ccae..3c81bd823e82 100644 --- a/packages/beacon-node/test/spec/presets/ssz_static.test.ts +++ b/packages/beacon-node/test/spec/presets/ssz_static.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import {it, vi} from "vitest"; +import {expect, it, vi} from "vitest"; import {Type} from "@chainsafe/ssz"; import {ssz} from "@lodestar/types"; import {ACTIVE_PRESET, ForkName, ForkLightClient, ForkExecution} from "@lodestar/params"; @@ -56,23 +56,26 @@ const sszStatic = (ssz.bellatrix as Types)[typeName] || (ssz.altair as Types)[typeName] || (ssz.phase0 as Types)[typeName]; - if (!sszType) { - throw Error(`No type for ${typeName}`); - } - const sszTypeNoUint = replaceUintTypeWithUintBigintType(sszType); + if (!sszType) { + expect.fail( + `Missing SSZ type definition for ${typeName}; this will prevent associated ssz_static tests to be executed` + ); + } else { + const sszTypeNoUint = replaceUintTypeWithUintBigintType(sszType); - for (const testCase of fs.readdirSync(testSuiteDirpath)) { - // Do not manually skip tests here, do it in packages/beacon-node/test/spec/presets/index.test.ts - it(testCase, function () { - // Mainnet must deal with big full states and hash each one multiple times - if (ACTIVE_PRESET === "mainnet") { - vi.setConfig({testTimeout: 30 * 1000}); - } + for (const testCase of fs.readdirSync(testSuiteDirpath)) { + // Do not manually skip tests here, do it in packages/beacon-node/test/spec/presets/index.test.ts + it(testCase, function () { + // Mainnet must deal with big full states and hash each one multiple times + if (ACTIVE_PRESET === "mainnet") { + vi.setConfig({testTimeout: 30 * 1000}); + } - const testData = parseSszStaticTestcase(path.join(testSuiteDirpath, testCase)); - runValidSszTest(sszTypeNoUint, testData); - }); + const testData = parseSszStaticTestcase(path.join(testSuiteDirpath, testCase)); + runValidSszTest(sszTypeNoUint, testData); + }); + } } }; From 22a2980853e88eed69e0fcc2be1f7818a9c51ae0 Mon Sep 17 00:00:00 2001 From: twoeths Date: Thu, 16 May 2024 21:09:28 +0300 Subject: [PATCH 43/51] fix: batch validation for electra attestations (#6788) --- .../src/api/impl/beacon/pool/index.ts | 4 +- .../src/chain/opPools/attestationPool.ts | 29 ++-- .../chain/seenCache/seenAttestationData.ts | 24 ++-- .../src/chain/validation/aggregateAndProof.ts | 2 +- .../src/chain/validation/attestation.ts | 135 +++++++++++------- .../src/network/processor/gossipHandlers.ts | 12 +- .../network/processor/gossipQueues/index.ts | 8 +- .../network/processor/gossipQueues/indexed.ts | 1 + packages/beacon-node/src/util/sszBytes.ts | 30 +++- .../perf/chain/validation/attestation.test.ts | 6 +- .../attestation/validateAttestation.test.ts | 26 ++-- .../test/unit/util/sszBytes.test.ts | 6 +- .../validator/src/services/attestation.ts | 4 +- .../validator/src/services/validatorStore.ts | 2 +- 14 files changed, 179 insertions(+), 110 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index 3f594fa4c3ae..19c11362c910 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -66,7 +66,7 @@ export function getBeaconPoolApi({ // when a validator is configured with multiple beacon node urls, this attestation data may come from another beacon node // and the block hasn't been in our forkchoice since we haven't seen / processing that block // see https://github.com/ChainSafe/lodestar/issues/5098 - const {indexedAttestation, subnet, attDataRootHex} = await validateGossipFnRetryUnknownRoot( + const {indexedAttestation, subnet, attDataRootHex, committeeIndex} = await validateGossipFnRetryUnknownRoot( validateFn, network, chain, @@ -75,7 +75,7 @@ export function getBeaconPoolApi({ ); if (network.shouldAggregate(subnet, slot)) { - const insertOutcome = chain.attestationPool.add(attestation, attDataRootHex); + const insertOutcome = chain.attestationPool.add(committeeIndex, attestation, attDataRootHex); metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome}); } diff --git a/packages/beacon-node/src/chain/opPools/attestationPool.ts b/packages/beacon-node/src/chain/opPools/attestationPool.ts index 7d6cca30056c..b647f2843497 100644 --- a/packages/beacon-node/src/chain/opPools/attestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/attestationPool.ts @@ -62,9 +62,10 @@ type CommitteeIndex = number; * receives and it can be triggered manually. */ export class AttestationPool { - private readonly attestationByRootBySlot = new MapDef>>( - () => new Map>() - ); + private readonly aggregateByIndexByRootBySlot = new MapDef< + Slot, + Map> + >(() => new Map>()); private lowestPermissibleSlot = 0; constructor( @@ -76,8 +77,10 @@ export class AttestationPool { /** Returns current count of pre-aggregated attestations with unique data */ getAttestationCount(): number { let attestationCount = 0; - for (const attestationByRoot of this.attestationByRootBySlot.values()) { - attestationCount += attestationByRoot.size; + for (const attestationByIndexByRoot of this.aggregateByIndexByRootBySlot.values()) { + for (const attestationByIndex of attestationByIndexByRoot.values()) { + attestationCount += attestationByIndex.size; + } } return attestationCount; } @@ -99,7 +102,7 @@ export class AttestationPool { * - Valid committeeIndex * - Valid data */ - add(attestation: allForks.Attestation, attDataRootHex: RootHex): InsertOutcome { + add(committeeIndex: CommitteeIndex, attestation: allForks.Attestation, attDataRootHex: RootHex): InsertOutcome { const slot = attestation.data.slot; const lowestPermissibleSlot = this.lowestPermissibleSlot; @@ -114,15 +117,11 @@ export class AttestationPool { } // Limit object per slot - const aggregateByRoot = this.attestationByRootBySlot.getOrDefault(slot); + const aggregateByRoot = this.aggregateByIndexByRootBySlot.getOrDefault(slot); if (aggregateByRoot.size >= MAX_ATTESTATIONS_PER_SLOT) { throw new OpPoolError({code: OpPoolErrorCode.REACHED_MAX_PER_SLOT}); } - const committeeIndex = isElectraAttestation(attestation) - ? // this attestation is added to pool after validation - attestation.committeeBits.getSingleTrueBit() - : attestation.data.index; // this should not happen because attestation should be validated before reaching this assert.notNull(committeeIndex, "Committee index should not be null in attestation pool"); @@ -147,7 +146,7 @@ export class AttestationPool { * For validator API to get an aggregate */ getAggregate(slot: Slot, committeeIndex: CommitteeIndex, dataRootHex: RootHex): allForks.Attestation | null { - const aggregate = this.attestationByRootBySlot.get(slot)?.get(dataRootHex)?.get(committeeIndex); + const aggregate = this.aggregateByIndexByRootBySlot.get(slot)?.get(dataRootHex)?.get(committeeIndex); if (!aggregate) { // TODO: Add metric for missing aggregates return null; @@ -161,7 +160,7 @@ export class AttestationPool { * By default, not interested in attestations in old slots, we only preaggregate attestations for the current slot. */ prune(clockSlot: Slot): void { - pruneBySlot(this.attestationByRootBySlot, clockSlot, SLOTS_RETAINED); + pruneBySlot(this.aggregateByIndexByRootBySlot, clockSlot, SLOTS_RETAINED); // by default preaggregateSlotDistance is 0, i.e only accept attestations in the same clock slot. this.lowestPermissibleSlot = Math.max(clockSlot - this.preaggregateSlotDistance, 0); } @@ -175,8 +174,8 @@ export class AttestationPool { const aggregateByRoots = bySlot === undefined - ? Array.from(this.attestationByRootBySlot.values()) - : [this.attestationByRootBySlot.get(bySlot)]; + ? Array.from(this.aggregateByIndexByRootBySlot.values()) + : [this.aggregateByIndexByRootBySlot.get(bySlot)]; for (const aggregateByRoot of aggregateByRoots) { if (aggregateByRoot) { diff --git a/packages/beacon-node/src/chain/seenCache/seenAttestationData.ts b/packages/beacon-node/src/chain/seenCache/seenAttestationData.ts index 9312f3b517a7..17343e386fd7 100644 --- a/packages/beacon-node/src/chain/seenCache/seenAttestationData.ts +++ b/packages/beacon-node/src/chain/seenCache/seenAttestationData.ts @@ -1,12 +1,16 @@ -import {phase0, RootHex, Slot} from "@lodestar/types"; +import {BitArray} from "@chainsafe/ssz"; +import {CommitteeIndex, phase0, RootHex, Slot} from "@lodestar/types"; import {MapDef} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; -import {AttDataBase64} from "../../util/sszBytes.js"; +import {SeenAttDataKey} from "../../util/sszBytes.js"; import {InsertOutcome} from "../opPools/types.js"; export type AttestationDataCacheEntry = { // part of shuffling data, so this does not take memory - committeeIndices: Uint32Array; + committeeValidatorIndices: Uint32Array; + // undefined for phase0 Attestation + committeeBits?: BitArray; + committeeIndex: CommitteeIndex; // IndexedAttestationData signing root, 32 bytes signingRoot: Uint8Array; // to be consumed by forkchoice and oppool @@ -38,12 +42,14 @@ const DEFAULT_MAX_CACHE_SIZE_PER_SLOT = 200; const DEFAULT_CACHE_SLOT_DISTANCE = 2; /** + * Cached seen AttestationData to improve gossip validation. For Electra, this still take into account attestationIndex + * even through it is moved outside of AttestationData. * As of April 2023, validating gossip attestation takes ~12% of cpu time for a node subscribing to all subnets on mainnet. * Having this cache help saves a lot of cpu time since most of the gossip attestations are on the same slot. */ export class SeenAttestationDatas { - private cacheEntryByAttDataBase64BySlot = new MapDef>( - () => new Map() + private cacheEntryByAttDataBase64BySlot = new MapDef>( + () => new Map() ); private lowestPermissibleSlot = 0; @@ -57,14 +63,14 @@ export class SeenAttestationDatas { } // TODO: Move InsertOutcome type definition to a common place - add(slot: Slot, attDataBase64: AttDataBase64, cacheEntry: AttestationDataCacheEntry): InsertOutcome { + add(slot: Slot, attDataKey: SeenAttDataKey, cacheEntry: AttestationDataCacheEntry): InsertOutcome { if (slot < this.lowestPermissibleSlot) { this.metrics?.seenCache.attestationData.reject.inc({reason: RejectReason.too_old}); return InsertOutcome.Old; } const cacheEntryByAttDataBase64 = this.cacheEntryByAttDataBase64BySlot.getOrDefault(slot); - if (cacheEntryByAttDataBase64.has(attDataBase64)) { + if (cacheEntryByAttDataBase64.has(attDataKey)) { this.metrics?.seenCache.attestationData.reject.inc({reason: RejectReason.already_known}); return InsertOutcome.AlreadyKnown; } @@ -74,11 +80,11 @@ export class SeenAttestationDatas { return InsertOutcome.ReachLimit; } - cacheEntryByAttDataBase64.set(attDataBase64, cacheEntry); + cacheEntryByAttDataBase64.set(attDataKey, cacheEntry); return InsertOutcome.NewData; } - get(slot: Slot, attDataBase64: AttDataBase64): AttestationDataCacheEntry | null { + get(slot: Slot, attDataBase64: SeenAttDataKey): AttestationDataCacheEntry | null { const cacheEntryByAttDataBase64 = this.cacheEntryByAttDataBase64BySlot.get(slot); const cacheEntry = cacheEntryByAttDataBase64?.get(attDataBase64); if (cacheEntry) { diff --git a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts index a1d7135716ce..91776fbdd558 100644 --- a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts @@ -168,7 +168,7 @@ async function validateAggregateAndProof( // [REJECT] The committee index is within the expected range // -- i.e. data.index < get_committee_count_per_slot(state, data.target.epoch) const committeeIndices = cachedAttData - ? cachedAttData.committeeIndices + ? cachedAttData.committeeValidatorIndices : getCommitteeIndices(shuffling, attSlot, attIndex); // [REJECT] The number of aggregation bits matches the committee size diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index 87e8c2a6b2bd..0141eaafbb06 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -1,5 +1,16 @@ -import {toHexString} from "@chainsafe/ssz"; -import {phase0, Epoch, Root, Slot, RootHex, ssz, allForks, electra} from "@lodestar/types"; +import {BitArray, toHexString} from "@chainsafe/ssz"; +import { + phase0, + Epoch, + Root, + Slot, + RootHex, + ssz, + allForks, + electra, + isElectraAttestation, + CommitteeIndex, +} from "@lodestar/types"; import {ProtoBlock} from "@lodestar/fork-choice"; import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, ForkName, ForkSeq, DOMAIN_BEACON_ATTESTER} from "@lodestar/params"; import { @@ -17,10 +28,9 @@ import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/in import {MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC} from "../../constants/index.js"; import {RegenCaller} from "../regen/index.js"; import { - AttDataBase64, + SeenAttDataKey, getAggregationBitsFromAttestationSerialized, - getAttDataBase64FromAttestationSerialized, - getCommitteeBitsFromAttestationSerialized, + getSeenAttDataKey, getSignatureFromAttestationSerialized, } from "../../util/sszBytes.js"; import {AttestationDataCacheEntry} from "../seenCache/seenAttestationData.js"; @@ -39,12 +49,13 @@ export type AttestationValidationResult = { indexedAttestation: allForks.IndexedAttestation; subnet: number; attDataRootHex: RootHex; + committeeIndex: CommitteeIndex; }; export type AttestationOrBytes = ApiAttestation | GossipAttestation; /** attestation from api */ -export type ApiAttestation = {attestation: phase0.Attestation; serializedData: null}; +export type ApiAttestation = {attestation: allForks.Attestation; serializedData: null}; /** attestation from gossip */ export type GossipAttestation = { @@ -52,7 +63,9 @@ export type GossipAttestation = { serializedData: Uint8Array; // available in NetworkProcessor since we check for unknown block root attestations attSlot: Slot; - attDataBase64?: string | null; + // for old LIFO linear gossip queue we don't have attDataBase64 + // for indexed gossip queue we have attDataBase64 + seenAttestationKey?: SeenAttDataKey | null; }; export type Step0Result = AttestationValidationResult & { @@ -83,7 +96,7 @@ export async function validateGossipAttestation( export async function validateGossipAttestationsSameAttData( fork: ForkName, chain: IBeaconChain, - attestationOrBytesArr: AttestationOrBytes[], + attestationOrBytesArr: GossipAttestation[], subnet: number, // for unit test, consumers do not need to pass this step0ValidationFn = validateGossipAttestationNoSignatureCheck @@ -251,15 +264,16 @@ async function validateGossipAttestationNoSignatureCheck( let attestationOrCache: | {attestation: allForks.Attestation; cache: null} | {attestation: null; cache: AttestationDataCacheEntry; serializedData: Uint8Array}; - let attDataBase64: AttDataBase64 | null = null; + let attDataKey: SeenAttDataKey | null = null; if (attestationOrBytes.serializedData) { // gossip const attSlot = attestationOrBytes.attSlot; - // for old LIFO linear gossip queue we don't have attDataBase64 - // for indexed gossip queue we have attDataBase64 - attDataBase64 = - attestationOrBytes.attDataBase64 ?? getAttDataBase64FromAttestationSerialized(attestationOrBytes.serializedData); - const cachedAttData = attDataBase64 !== null ? chain.seenAttestationDatas.get(attSlot, attDataBase64) : null; + attDataKey = + // we always have seenAttestationKey from the IndexedGossipQueue, getSeenAttDataKey() just for backward + // compatible in case beaconAttestationBatchValidation is false + // TODO: remove beaconAttestationBatchValidation flag since the batch attestation is stable + attestationOrBytes.seenAttestationKey ?? getSeenAttDataKey(ForkSeq[fork], attestationOrBytes.serializedData); + const cachedAttData = attDataKey !== null ? chain.seenAttestationDatas.get(attSlot, attDataKey) : null; if (cachedAttData === null) { const attestation = sszDeserializeAttestation(fork, attestationOrBytes.serializedData); // only deserialize on the first AttestationData that's not cached @@ -269,7 +283,7 @@ async function validateGossipAttestationNoSignatureCheck( } } else { // api - attDataBase64 = null; + attDataKey = null; attestationOrCache = {attestation: attestationOrBytes.attestation, cache: null}; } @@ -280,29 +294,33 @@ async function validateGossipAttestationNoSignatureCheck( const attEpoch = computeEpochAtSlot(attSlot); const attTarget = attData.target; const targetEpoch = attTarget.epoch; + let committeeIndex; + if (attestationOrCache.attestation) { + if (isElectraAttestation(attestationOrCache.attestation)) { + // api or first time validation of a gossip attestation + const {committeeBits} = attestationOrCache.attestation; + // throw in both in case of undefined and null + if (committeeBits == null) { + throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.INVALID_SERIALIZED_BYTES}); + } - let attIndex; - if (ForkSeq[fork] >= ForkSeq.electra) { - const committeeBits = attestationOrCache.attestation - ? (attestationOrCache.attestation as electra.Attestation).committeeBits - : getCommitteeBitsFromAttestationSerialized(attestationOrCache.serializedData); - - if (committeeBits === null) { - throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.INVALID_SERIALIZED_BYTES}); - } - - attIndex = committeeBits.getSingleTrueBit(); - // [REJECT] len(committee_indices) == 1, where committee_indices = get_committee_indices(aggregate) - if (attIndex === null) { - throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET}); - } + committeeIndex = committeeBits.getSingleTrueBit(); + // [REJECT] len(committee_indices) == 1, where committee_indices = get_committee_indices(aggregate) + if (committeeIndex === null) { + throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET}); + } - // [REJECT] aggregate.data.index == 0 - if (attData.index !== 0) { - throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}); + // [REJECT] aggregate.data.index == 0 + if (attData.index !== 0) { + throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}); + } + } else { + // phase0 attestation + committeeIndex = attData.index; } } else { - attIndex = attData.index; + // found a seen AttestationData + committeeIndex = attestationOrCache.cache.committeeIndex; } chain.metrics?.gossipAttestation.attestationSlotToClockSlot.observe( @@ -343,11 +361,11 @@ async function validateGossipAttestationNoSignatureCheck( }); } - let committeeIndices: Uint32Array; + let committeeValidatorIndices: Uint32Array; let getSigningRoot: () => Uint8Array; let expectedSubnet: number; if (attestationOrCache.cache) { - committeeIndices = attestationOrCache.cache.committeeIndices; + committeeValidatorIndices = attestationOrCache.cache.committeeValidatorIndices; const signingRoot = attestationOrCache.cache.signingRoot; getSigningRoot = () => signingRoot; expectedSubnet = attestationOrCache.cache.subnet; @@ -389,17 +407,17 @@ async function validateGossipAttestationNoSignatureCheck( // [REJECT] The committee index is within the expected range // -- i.e. data.index < get_committee_count_per_slot(state, data.target.epoch) - committeeIndices = getCommitteeIndices(shuffling, attSlot, attIndex); + committeeValidatorIndices = getCommitteeIndices(shuffling, attSlot, committeeIndex); getSigningRoot = () => getAttestationDataSigningRoot(chain.config, attData); - expectedSubnet = computeSubnetForSlot(shuffling, attSlot, attIndex); + expectedSubnet = computeSubnetForSlot(shuffling, attSlot, committeeIndex); } - const validatorIndex = committeeIndices[bitIndex]; + const validatorIndex = committeeValidatorIndices[bitIndex]; // [REJECT] The number of aggregation bits matches the committee size // -- i.e. len(attestation.aggregation_bits) == len(get_beacon_committee(state, data.slot, data.index)). // > TODO: Is this necessary? Lighthouse does not do this check. - if (aggregationBits.bitLen !== committeeIndices.length) { + if (aggregationBits.bitLen !== committeeValidatorIndices.length) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS, }); @@ -445,6 +463,7 @@ async function validateGossipAttestationNoSignatureCheck( }); } + let committeeBits: BitArray | undefined = undefined; if (attestationOrCache.cache) { // there could be up to 6% of cpu time to compute signing root if we don't clone the signature set signatureSet = createSingleSignatureSetFromComponents( @@ -453,6 +472,7 @@ async function validateGossipAttestationNoSignatureCheck( signature ); attDataRootHex = attestationOrCache.cache.attDataRootHex; + committeeBits = attestationOrCache.cache.committeeBits; } else { signatureSet = createSingleSignatureSetFromComponents( chain.index2pubkey[validatorIndex], @@ -462,9 +482,15 @@ async function validateGossipAttestationNoSignatureCheck( // add cached attestation data before verifying signature attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(attData)); - if (attDataBase64) { - chain.seenAttestationDatas.add(attSlot, attDataBase64, { - committeeIndices, + // if attestation is phase0 the committeeBits is undefined anyway + committeeBits = isElectraAttestation(attestationOrCache.attestation) + ? attestationOrCache.attestation.committeeBits.clone() + : undefined; + if (attDataKey) { + chain.seenAttestationDatas.add(attSlot, attDataKey, { + committeeValidatorIndices, + committeeBits, + committeeIndex, signingRoot: signatureSet.signingRoot, subnet: expectedSubnet, // precompute this to be used in forkchoice @@ -486,14 +512,21 @@ async function validateGossipAttestationNoSignatureCheck( ? (indexedAttestationContent as electra.IndexedAttestation) : (indexedAttestationContent as phase0.IndexedAttestation); - const attestation: allForks.Attestation = attestationOrCache.attestation - ? attestationOrCache.attestation - : { - aggregationBits, - data: attData, - signature, - }; - return {attestation, indexedAttestation, subnet: expectedSubnet, attDataRootHex, signatureSet, validatorIndex}; + const attestation: allForks.Attestation = attestationOrCache.attestation ?? { + aggregationBits, + data: attData, + committeeBits, + signature, + }; + return { + attestation, + indexedAttestation, + subnet: expectedSubnet, + attDataRootHex, + signatureSet, + validatorIndex, + committeeIndex, + }; } /** diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 55de125fed29..2ab64895e3b9 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -38,8 +38,8 @@ import { AggregateAndProofValidationResult, validateGossipAttestationsSameAttData, validateGossipAttestation, - AttestationOrBytes, AttestationValidationResult, + GossipAttestation, } from "../../chain/validation/index.js"; import {NetworkEvent, NetworkEventBus} from "../events.js"; import {PeerAction} from "../peers/index.js"; @@ -488,14 +488,14 @@ function getDefaultHandlers(modules: ValidatorFnsModules, options: GossipHandler } // Handler - const {indexedAttestation, attDataRootHex, attestation} = validationResult; + const {indexedAttestation, attDataRootHex, attestation, committeeIndex} = validationResult; metrics?.registerGossipUnaggregatedAttestation(seenTimestampSec, indexedAttestation); try { // Node may be subscribe to extra subnets (long-lived random subnets). For those, validate the messages // but don't add to attestation pool, to save CPU and RAM if (aggregatorTracker.shouldAggregate(subnet, indexedAttestation.data.slot)) { - const insertOutcome = chain.attestationPool.add(attestation, attDataRootHex); + const insertOutcome = chain.attestationPool.add(committeeIndex, attestation, attDataRootHex); metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome}); } } catch (e) { @@ -680,7 +680,7 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp serializedData: param.gossipData.serializedData, attSlot: param.gossipData.msgSlot, attDataBase64: param.gossipData.indexed, - })) as AttestationOrBytes[]; + })) as GossipAttestation[]; const {results: validationResults, batchableBls} = await validateGossipAttestationsSameAttData( fork, chain, @@ -696,14 +696,14 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp results.push(null); // Handler - const {indexedAttestation, attDataRootHex, attestation} = validationResult.result; + const {indexedAttestation, attDataRootHex, attestation, committeeIndex} = validationResult.result; metrics?.registerGossipUnaggregatedAttestation(gossipHandlerParams[i].seenTimestampSec, indexedAttestation); try { // Node may be subscribe to extra subnets (long-lived random subnets). For those, validate the messages // but don't add to attestation pool, to save CPU and RAM if (aggregatorTracker.shouldAggregate(subnet, indexedAttestation.data.slot)) { - const insertOutcome = chain.attestationPool.add(attestation, attDataRootHex); + const insertOutcome = chain.attestationPool.add(committeeIndex, attestation, attDataRootHex); metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome}); } } catch (e) { diff --git a/packages/beacon-node/src/network/processor/gossipQueues/index.ts b/packages/beacon-node/src/network/processor/gossipQueues/index.ts index 366b23b30679..b38ee74279ca 100644 --- a/packages/beacon-node/src/network/processor/gossipQueues/index.ts +++ b/packages/beacon-node/src/network/processor/gossipQueues/index.ts @@ -1,7 +1,8 @@ import {mapValues} from "@lodestar/utils"; +import {ForkSeq} from "@lodestar/params"; import {GossipType} from "../../gossip/interface.js"; import {PendingGossipsubMessage} from "../types.js"; -import {getAttDataBase64FromAttestationSerialized} from "../../../util/sszBytes.js"; +import {getSeenAttDataKey} from "../../../util/sszBytes.js"; import {LinearGossipQueue} from "./linear.js"; import { DropType, @@ -86,7 +87,10 @@ const indexedGossipQueueOpts: { } = { [GossipType.beacon_attestation]: { maxLength: 24576, - indexFn: (item: PendingGossipsubMessage) => getAttDataBase64FromAttestationSerialized(item.msg.data), + indexFn: (item: PendingGossipsubMessage) => { + const {topic, msg} = item; + return getSeenAttDataKey(ForkSeq[topic.fork], msg.data); + }, minChunkSize: MIN_SIGNATURE_SETS_TO_BATCH_VERIFY, maxChunkSize: MAX_GOSSIP_ATTESTATION_BATCH_SIZE, }, diff --git a/packages/beacon-node/src/network/processor/gossipQueues/indexed.ts b/packages/beacon-node/src/network/processor/gossipQueues/indexed.ts index 4e29a52173f0..8edba7dfaadb 100644 --- a/packages/beacon-node/src/network/processor/gossipQueues/indexed.ts +++ b/packages/beacon-node/src/network/processor/gossipQueues/indexed.ts @@ -84,6 +84,7 @@ export class IndexedGossipQueueMinSize= ForkSeq.electra ? getSeenAttDataKeyElectra(data) : getSeenAttDataKeyPhase0(data); +} + +/** + * Extract attestation data + committeeBits base64 from electra attestation serialized bytes. + * Return null if data is not long enough to extract attestation data. + */ +export function getSeenAttDataKeyElectra(electraAttestationBytes: Uint8Array): AttDataCommitteeBitsBase64 | null { + const startIndex = VARIABLE_FIELD_OFFSET; + const seenKeyLength = ATTESTATION_DATA_SIZE + COMMITTEE_BITS_SIZE; + + if (electraAttestationBytes.length < startIndex + seenKeyLength) { + return null; + } + + return Buffer.from(electraAttestationBytes.subarray(startIndex, startIndex + seenKeyLength)).toString("base64"); +} + +/** + * Extract attestation data base64 from phase0 attestation serialized bytes. * Return null if data is not long enough to extract attestation data. */ -export function getAttDataBase64FromAttestationSerialized(data: Uint8Array): AttDataBase64 | null { +export function getSeenAttDataKeyPhase0(data: Uint8Array): AttDataBase64 | null { if (data.length < VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE) { return null; } diff --git a/packages/beacon-node/test/perf/chain/validation/attestation.test.ts b/packages/beacon-node/test/perf/chain/validation/attestation.test.ts index 5fce9a342509..8f462609c063 100644 --- a/packages/beacon-node/test/perf/chain/validation/attestation.test.ts +++ b/packages/beacon-node/test/perf/chain/validation/attestation.test.ts @@ -5,7 +5,7 @@ import {ssz} from "@lodestar/types"; import {generateTestCachedBeaconStateOnlyValidators} from "../../../../../state-transition/test/perf/util.js"; import {validateAttestation, validateGossipAttestationsSameAttData} from "../../../../src/chain/validation/index.js"; import {getAttestationValidData} from "../../../utils/validationData/attestation.js"; -import {getAttDataBase64FromAttestationSerialized} from "../../../../src/util/sszBytes.js"; +import {getSeenAttDataKeyPhase0} from "../../../../src/util/sszBytes.js"; describe("validate gossip attestation", () => { setBenchOpts({ @@ -42,7 +42,7 @@ describe("validate gossip attestation", () => { attestation: null, serializedData, attSlot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), }, subnet0 ); @@ -67,7 +67,7 @@ describe("validate gossip attestation", () => { attestation: null, serializedData, attSlot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + attDataBase64: getSeenAttDataKeyPhase0(serializedData), }; }); diff --git a/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts index 56aab699f4f7..c7d9dc775c85 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts @@ -12,7 +12,7 @@ import { validateApiAttestation, validateAttestation, } from "../../../../../src/chain/validation/index.js"; -import {getAttDataBase64FromAttestationSerialized} from "../../../../../src/util/sszBytes.js"; +import {getSeenAttDataKeyPhase0} from "../../../../../src/util/sszBytes.js"; import {memoOnce} from "../../../../utils/cache.js"; import {expectRejectedWithLodestarError} from "../../../../utils/errors.js"; import {AttestationValidDataOpts, getAttestationValidData} from "../../../../utils/validationData/attestation.js"; @@ -52,7 +52,7 @@ describe("validateAttestation", () => { const {chain, subnet} = getValidData(); await expectGossipError( chain, - {attestation: null, serializedData: Buffer.alloc(0), attSlot: 0, attDataBase64: "invalid"}, + {attestation: null, serializedData: Buffer.alloc(0), attSlot: 0, seenAttestationDataKey: "invalid"}, subnet, GossipErrorCode.INVALID_SERIALIZED_BYTES_ERROR_CODE ); @@ -72,7 +72,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.BAD_TARGET_EPOCH @@ -91,7 +91,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.PAST_SLOT @@ -110,7 +110,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.FUTURE_SLOT @@ -135,7 +135,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET @@ -155,7 +155,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET @@ -179,7 +179,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT @@ -199,7 +199,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.INVALID_TARGET_ROOT @@ -226,7 +226,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS @@ -245,7 +245,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), }, invalidSubnet, AttestationErrorCode.INVALID_SUBNET_ID @@ -265,7 +265,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.ATTESTATION_ALREADY_KNOWN @@ -287,7 +287,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.INVALID_SIGNATURE diff --git a/packages/beacon-node/test/unit/util/sszBytes.test.ts b/packages/beacon-node/test/unit/util/sszBytes.test.ts index d0dc150cd792..8c277300d834 100644 --- a/packages/beacon-node/test/unit/util/sszBytes.test.ts +++ b/packages/beacon-node/test/unit/util/sszBytes.test.ts @@ -4,7 +4,7 @@ import {allForks, deneb, Epoch, isElectraAttestation, phase0, RootHex, Slot, ssz import {fromHex, toHex} from "@lodestar/utils"; import {ForkName, MAX_COMMITTEES_PER_SLOT} from "@lodestar/params"; import { - getAttDataBase64FromAttestationSerialized, + getSeenAttDataKeyPhase0, getAttDataBase64FromSignedAggregateAndProofSerialized, getAggregationBitsFromAttestationSerialized as getAggregationBitsFromAttestationSerialized, getBlockRootFromAttestationSerialized, @@ -62,7 +62,7 @@ describe("attestation SSZ serialized picking", () => { } const attDataBase64 = ssz.phase0.AttestationData.serialize(attestation.data); - expect(getAttDataBase64FromAttestationSerialized(bytes)).toBe(Buffer.from(attDataBase64).toString("base64")); + expect(getSeenAttDataKeyPhase0(bytes)).toBe(Buffer.from(attDataBase64).toString("base64")); }); } @@ -83,7 +83,7 @@ describe("attestation SSZ serialized picking", () => { it("getAttDataBase64FromAttestationSerialized - invalid data", () => { const invalidAttDataBase64DataSizes = [0, 4, 100, 128, 131]; for (const size of invalidAttDataBase64DataSizes) { - expect(getAttDataBase64FromAttestationSerialized(Buffer.alloc(size))).toBeNull(); + expect(getSeenAttDataKeyPhase0(Buffer.alloc(size))).toBeNull(); } }); diff --git a/packages/validator/src/services/attestation.ts b/packages/validator/src/services/attestation.ts index 7be2c74cd21d..e6ea81d1b1d6 100644 --- a/packages/validator/src/services/attestation.ts +++ b/packages/validator/src/services/attestation.ts @@ -1,5 +1,5 @@ import {toHexString} from "@chainsafe/ssz"; -import {allForks, BLSSignature, phase0, Slot, ssz} from "@lodestar/types"; +import {allForks, BLSSignature, electra, isElectraAttestation, phase0, Slot, ssz} from "@lodestar/types"; import {computeEpochAtSlot, isAggregatorFromCommitteeLength} from "@lodestar/state-transition"; import {sleep} from "@lodestar/utils"; import {ApiClient, routes} from "@lodestar/api"; @@ -182,7 +182,7 @@ export class AttestationService { attestationNoCommittee: phase0.AttestationData, duties: AttDutyAndProof[] ): Promise { - const signedAttestations: phase0.Attestation[] = []; + const signedAttestations: allForks.Attestation[] = []; const headRootHex = toHexString(attestationNoCommittee.beaconBlockRoot); const currentEpoch = computeEpochAtSlot(slot); const isAfterElectra = currentEpoch >= this.config.ELECTRA_FORK_EPOCH; diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 04923cbddf30..f36032004aa8 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -531,7 +531,7 @@ export class ValidatorStore { data: attestationData, committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, duty.committeeIndex), signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage), - } as electra.Attestation; + }; } else { return { aggregationBits: BitArray.fromSingleBit(duty.committeeLength, duty.validatorCommitteeIndex), From 29f066b92965c4a0db2b471686bb1949cb1d863f Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 16 May 2024 21:23:52 +0300 Subject: [PATCH 44/51] fix: update withdrawal request container to match consensus spec (#6797) --- packages/beacon-node/src/execution/engine/types.ts | 4 ++-- .../src/block/processExecutionLayerWithdrawalRequest.ts | 2 +- packages/types/src/electra/sszTypes.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index f957722c0955..9db6717c4749 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -419,7 +419,7 @@ export function serializeExecutionLayerWithdrawalRequest( ): ExecutionLayerWithdrawalRequestRpc { return { sourceAddress: bytesToData(withdrawalRequest.sourceAddress), - validatorPublicKey: bytesToData(withdrawalRequest.validatorPublicKey), + validatorPublicKey: bytesToData(withdrawalRequest.validatorPubkey), amount: numToQuantity(withdrawalRequest.amount), }; } @@ -429,7 +429,7 @@ export function deserializeExecutionLayerWithdrawalRequest( ): electra.ExecutionLayerWithdrawalRequest { return { sourceAddress: dataToBytes(withdrawalRequest.sourceAddress, 20), - validatorPublicKey: dataToBytes(withdrawalRequest.validatorPublicKey, 48), + validatorPubkey: dataToBytes(withdrawalRequest.validatorPublicKey, 48), amount: quantityToNum(withdrawalRequest.amount), }; } diff --git a/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts b/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts index 935acd522da1..e02129a09d05 100644 --- a/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts +++ b/packages/state-transition/src/block/processExecutionLayerWithdrawalRequest.ts @@ -32,7 +32,7 @@ export function processExecutionLayerWithdrawalRequest( // bail out if validator is not in beacon state // note that we don't need to check for 6110 unfinalized vals as they won't be eligible for withdraw/exit anyway - const validatorIndex = pubkey2index.get(executionLayerWithdrawalRequest.validatorPublicKey); + const validatorIndex = pubkey2index.get(executionLayerWithdrawalRequest.validatorPubkey); if (validatorIndex === undefined) { return; } diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index 0a0c0f6aeefb..a8249156c7b9 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -128,7 +128,7 @@ export const DepositReceipts = new ListCompositeType(DepositReceipt, MAX_DEPOSIT export const ExecutionLayerWithdrawalRequest = new ContainerType( { sourceAddress: ExecutionAddress, - validatorPublicKey: BLSPubkey, + validatorPubkey: BLSPubkey, amount: UintNum64, }, {typeName: "ExecutionLayerWithdrawalRequest", jsonCase: "eth2"} From 65627c1387398b0f2887ea45b91e1fc296c89a2f Mon Sep 17 00:00:00 2001 From: twoeths Date: Fri, 17 May 2024 18:47:58 +0300 Subject: [PATCH 45/51] fix: get seen AttData key from SignedAggregateAndProof electra (#6802) * fix: get seen AttData key from SignedAggregateAndProof electra * chore: revert the naming change to COMMITTEE_BITS_SIZE and add comment * fix: add toBase64() util --- .../src/chain/validation/aggregateAndProof.ts | 10 ++- packages/beacon-node/src/util/sszBytes.ts | 43 ++++++++-- .../test/unit/util/sszBytes.test.ts | 84 +++++++++++++++++-- 3 files changed, 120 insertions(+), 17 deletions(-) diff --git a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts index 91776fbdd558..782cfd070390 100644 --- a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts @@ -9,7 +9,9 @@ import { import {IBeaconChain} from ".."; import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/index.js"; import {RegenCaller} from "../regen/index.js"; -import {getAttDataBase64FromSignedAggregateAndProofSerialized} from "../../util/sszBytes.js"; +import { + getSeenAttDataKeyFromSignedAggregateAndProof, +} from "../../util/sszBytes.js"; import {getSelectionProofSignatureSet, getAggregateAndProofSignatureSet} from "./signatureSets/index.js"; import { getAttestationDataSigningRoot, @@ -71,8 +73,10 @@ async function validateAggregateAndProof( const attData = aggregate.data; const attSlot = attData.slot; - const attDataBase64 = serializedData ? getAttDataBase64FromSignedAggregateAndProofSerialized(serializedData) : null; - const cachedAttData = attDataBase64 ? chain.seenAttestationDatas.get(attSlot, attDataBase64) : null; + const seenAttDataKey = serializedData + ? getSeenAttDataKeyFromSignedAggregateAndProof(ForkSeq[fork], serializedData) + : null; + const cachedAttData = seenAttDataKey ? chain.seenAttestationDatas.get(attSlot, seenAttDataKey) : null; let attIndex; if (ForkSeq[fork] >= ForkSeq.electra) { diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index 0eb72a3e027d..b3f1c4d9432b 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -42,6 +42,7 @@ const ATTESTATION_BEACON_BLOCK_ROOT_OFFSET = VARIABLE_FIELD_OFFSET + 8 + 8; const ROOT_SIZE = 32; const SLOT_SIZE = 8; const ATTESTATION_DATA_SIZE = 128; +// MAX_COMMITTEES_PER_SLOT is in bit, need to convert to byte const COMMITTEE_BITS_SIZE = Math.max(Math.ceil(MAX_COMMITTEES_PER_SLOT / 8), 1); const SIGNATURE_SIZE = 96; @@ -88,7 +89,7 @@ export function getSeenAttDataKeyElectra(electraAttestationBytes: Uint8Array): A return null; } - return Buffer.from(electraAttestationBytes.subarray(startIndex, startIndex + seenKeyLength)).toString("base64"); + return toBase64(electraAttestationBytes.subarray(startIndex, startIndex + seenKeyLength)); } /** @@ -171,8 +172,9 @@ const SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET = AGGREGATE_OFFSET + VARIABLE_FIELD const SIGNED_AGGREGATE_AND_PROOF_BLOCK_ROOT_OFFSET = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + 8 + 8; /** - * Extract slot from signed aggregate and proof serialized bytes. - * Return null if data is not long enough to extract slot. + * Extract slot from signed aggregate and proof serialized bytes + * Return null if data is not long enough to extract slot + * This works for both phase + electra */ export function getSlotFromSignedAggregateAndProofSerialized(data: Uint8Array): Slot | null { if (data.length < SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + SLOT_SIZE) { @@ -183,8 +185,9 @@ export function getSlotFromSignedAggregateAndProofSerialized(data: Uint8Array): } /** - * Extract block root from signed aggregate and proof serialized bytes. - * Return null if data is not long enough to extract block root. + * Extract block root from signed aggregate and proof serialized bytes + * Return null if data is not long enough to extract block root + * This works for both phase + electra */ export function getBlockRootFromSignedAggregateAndProofSerialized(data: Uint8Array): BlockRootHex | null { if (data.length < SIGNED_AGGREGATE_AND_PROOF_BLOCK_ROOT_OFFSET + ROOT_SIZE) { @@ -199,11 +202,39 @@ export function getBlockRootFromSignedAggregateAndProofSerialized(data: Uint8Arr ); } +/** + * Extract attestation data key from SignedAggregateAndProof Uint8Array to use cached data from SeenAttestationDatas + */ +export function getSeenAttDataKeyFromSignedAggregateAndProof( + forkSeq: ForkSeq, + data: Uint8Array +): SeenAttDataKey | null { + return forkSeq >= ForkSeq.electra + ? getSeenAttDataKeyFromSignedAggregateAndProofElectra(data) + : getSeenAttDataKeyFromSignedAggregateAndProofPhase0(data); +} + +/** + * Extract AttestationData + CommitteeBits from SignedAggregateAndProof for electra + * Return null if data is not long enough + */ +export function getSeenAttDataKeyFromSignedAggregateAndProofElectra(data: Uint8Array): SeenAttDataKey | null { + const startIndex = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET; + const endIndex = startIndex + ATTESTATION_DATA_SIZE + COMMITTEE_BITS_SIZE; + + if (data.length < endIndex) { + return null; + } + + // base64 is a bit efficient than hex + return toBase64(data.subarray(startIndex, endIndex)); +} + /** * Extract attestation data base64 from signed aggregate and proof serialized bytes. * Return null if data is not long enough to extract attestation data. */ -export function getAttDataBase64FromSignedAggregateAndProofSerialized(data: Uint8Array): AttDataBase64 | null { +export function getSeenAttDataKeyFromSignedAggregateAndProofPhase0(data: Uint8Array): AttDataBase64 | null { if (data.length < SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE) { return null; } diff --git a/packages/beacon-node/test/unit/util/sszBytes.test.ts b/packages/beacon-node/test/unit/util/sszBytes.test.ts index 8c277300d834..03e4a7f7a71d 100644 --- a/packages/beacon-node/test/unit/util/sszBytes.test.ts +++ b/packages/beacon-node/test/unit/util/sszBytes.test.ts @@ -1,11 +1,11 @@ import {describe, it, expect} from "vitest"; import {BitArray} from "@chainsafe/ssz"; -import {allForks, deneb, Epoch, isElectraAttestation, phase0, RootHex, Slot, ssz} from "@lodestar/types"; +import {allForks, deneb, electra, Epoch, isElectraAttestation, phase0, RootHex, Slot, ssz} from "@lodestar/types"; import {fromHex, toHex} from "@lodestar/utils"; import {ForkName, MAX_COMMITTEES_PER_SLOT} from "@lodestar/params"; import { getSeenAttDataKeyPhase0, - getAttDataBase64FromSignedAggregateAndProofSerialized, + getSeenAttDataKeyFromSignedAggregateAndProofPhase0, getAggregationBitsFromAttestationSerialized as getAggregationBitsFromAttestationSerialized, getBlockRootFromAttestationSerialized, getBlockRootFromSignedAggregateAndProofSerialized, @@ -15,6 +15,7 @@ import { getSlotFromSignedBeaconBlockSerialized, getSlotFromBlobSidecarSerialized, getCommitteeBitsFromAttestationSerialized, + getSeenAttDataKeyFromSignedAggregateAndProofElectra, } from "../../../src/util/sszBytes.js"; describe("attestation SSZ serialized picking", () => { @@ -104,16 +105,15 @@ describe("attestation SSZ serialized picking", () => { }); }); -describe("aggregateAndProof SSZ serialized picking", () => { +describe("phase0 SignedAggregateAndProof SSZ serialized picking", () => { const testCases: phase0.SignedAggregateAndProof[] = [ ssz.phase0.SignedAggregateAndProof.defaultValue(), - signedAggregateAndProofFromValues( + phase0SignedAggregateAndProofFromValues( 4_000_000, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 200_00, "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffffffffff" ), - ssz.electra.SignedAggregateAndProof.defaultValue(), ]; for (const [i, signedAggregateAndProof] of testCases.entries()) { @@ -128,7 +128,7 @@ describe("aggregateAndProof SSZ serialized picking", () => { ); const attDataBase64 = ssz.phase0.AttestationData.serialize(signedAggregateAndProof.message.aggregate.data); - expect(getAttDataBase64FromSignedAggregateAndProofSerialized(bytes)).toBe( + expect(getSeenAttDataKeyFromSignedAggregateAndProofPhase0(bytes)).toBe( Buffer.from(attDataBase64).toString("base64") ); }); @@ -151,7 +151,60 @@ describe("aggregateAndProof SSZ serialized picking", () => { it("getAttDataBase64FromSignedAggregateAndProofSerialized - invalid data", () => { const invalidAttDataBase64DataSizes = [0, 4, 100, 128, 339]; for (const size of invalidAttDataBase64DataSizes) { - expect(getAttDataBase64FromSignedAggregateAndProofSerialized(Buffer.alloc(size))).toBeNull(); + expect(getSeenAttDataKeyFromSignedAggregateAndProofPhase0(Buffer.alloc(size))).toBeNull(); + } + }); +}); + +describe("electra SignedAggregateAndProof SSZ serialized picking", () => { + const testCases: electra.SignedAggregateAndProof[] = [ + ssz.electra.SignedAggregateAndProof.defaultValue(), + electraSignedAggregateAndProofFromValues( + 4_000_000, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + 200_00, + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffffffffff" + ), + ]; + + for (const [i, signedAggregateAndProof] of testCases.entries()) { + it(`signedAggregateAndProof ${i}`, () => { + const bytes = ssz.electra.SignedAggregateAndProof.serialize(signedAggregateAndProof); + + expect(getSlotFromSignedAggregateAndProofSerialized(bytes)).toBe( + signedAggregateAndProof.message.aggregate.data.slot + ); + expect(getBlockRootFromSignedAggregateAndProofSerialized(bytes)).toBe( + toHex(signedAggregateAndProof.message.aggregate.data.beaconBlockRoot) + ); + + const attDataBase64 = ssz.phase0.AttestationData.serialize(signedAggregateAndProof.message.aggregate.data); + const committeeBits = ssz.electra.CommitteeBits.serialize( + signedAggregateAndProof.message.aggregate.committeeBits + ); + const seenKey = Buffer.concat([attDataBase64, committeeBits]).toString("base64"); + expect(getSeenAttDataKeyFromSignedAggregateAndProofElectra(bytes)).toBe(seenKey); + }); + } + + it("getSlotFromSignedAggregateAndProofSerialized - invalid data", () => { + const invalidSlotDataSizes = [0, 4, 11]; + for (const size of invalidSlotDataSizes) { + expect(getSlotFromSignedAggregateAndProofSerialized(Buffer.alloc(size))).toBeNull(); + } + }); + + it("getBlockRootFromSignedAggregateAndProofSerialized - invalid data", () => { + const invalidBlockRootDataSizes = [0, 4, 20, 227]; + for (const size of invalidBlockRootDataSizes) { + expect(getBlockRootFromSignedAggregateAndProofSerialized(Buffer.alloc(size))).toBeNull(); + } + }); + + it("getAttDataBase64FromSignedAggregateAndProofSerialized - invalid data", () => { + const invalidAttDataBase64DataSizes = [0, 4, 100, 128, 339]; + for (const size of invalidAttDataBase64DataSizes) { + expect(getSeenAttDataKeyFromSignedAggregateAndProofPhase0(Buffer.alloc(size))).toBeNull(); } }); }); @@ -206,7 +259,7 @@ function attestationFromValues( return attestation; } -function signedAggregateAndProofFromValues( +function phase0SignedAggregateAndProofFromValues( slot: Slot, blockRoot: RootHex, targetEpoch: Epoch, @@ -220,6 +273,21 @@ function signedAggregateAndProofFromValues( return signedAggregateAndProof; } +function electraSignedAggregateAndProofFromValues( + slot: Slot, + blockRoot: RootHex, + targetEpoch: Epoch, + targetRoot: RootHex +): electra.SignedAggregateAndProof { + const signedAggregateAndProof = ssz.electra.SignedAggregateAndProof.defaultValue(); + signedAggregateAndProof.message.aggregate.data.slot = slot; + signedAggregateAndProof.message.aggregate.data.beaconBlockRoot = fromHex(blockRoot); + signedAggregateAndProof.message.aggregate.data.target.epoch = targetEpoch; + signedAggregateAndProof.message.aggregate.data.target.root = fromHex(targetRoot); + signedAggregateAndProof.message.aggregate.committeeBits = BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, 1); + return signedAggregateAndProof; +} + function signedBeaconBlockFromValues(slot: Slot): phase0.SignedBeaconBlock { const signedBeaconBlock = ssz.phase0.SignedBeaconBlock.defaultValue(); signedBeaconBlock.message.slot = slot; From aadd017836d30c48ea522c94cb03c5cd8fa52fe5 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 23 May 2024 21:56:47 +0200 Subject: [PATCH 46/51] test: only skip ssz_static tests associated to missing type (#6798) * test: only skip ssz_static tests associated to missing type * More detailed error message if type is not defined --- .../test/spec/presets/ssz_static.test.ts | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/beacon-node/test/spec/presets/ssz_static.test.ts b/packages/beacon-node/test/spec/presets/ssz_static.test.ts index 3c81bd823e82..a3a9c8069606 100644 --- a/packages/beacon-node/test/spec/presets/ssz_static.test.ts +++ b/packages/beacon-node/test/spec/presets/ssz_static.test.ts @@ -57,25 +57,28 @@ const sszStatic = (ssz.altair as Types)[typeName] || (ssz.phase0 as Types)[typeName]; + it(`${fork} - ${typeName} type exists`, function () { + expect(sszType).toEqualWithMessage(expect.any(Type), `SSZ type ${typeName} for fork ${fork} is not defined`); + }); + if (!sszType) { - expect.fail( - `Missing SSZ type definition for ${typeName}; this will prevent associated ssz_static tests to be executed` - ); - } else { - const sszTypeNoUint = replaceUintTypeWithUintBigintType(sszType); + // Return instead of throwing an error to only skip ssz_static tests associated to missing type + return; + } + + const sszTypeNoUint = replaceUintTypeWithUintBigintType(sszType); - for (const testCase of fs.readdirSync(testSuiteDirpath)) { - // Do not manually skip tests here, do it in packages/beacon-node/test/spec/presets/index.test.ts - it(testCase, function () { - // Mainnet must deal with big full states and hash each one multiple times - if (ACTIVE_PRESET === "mainnet") { - vi.setConfig({testTimeout: 30 * 1000}); - } + for (const testCase of fs.readdirSync(testSuiteDirpath)) { + // Do not manually skip tests here, do it in packages/beacon-node/test/spec/presets/index.test.ts + it(testCase, function () { + // Mainnet must deal with big full states and hash each one multiple times + if (ACTIVE_PRESET === "mainnet") { + vi.setConfig({testTimeout: 30 * 1000}); + } - const testData = parseSszStaticTestcase(path.join(testSuiteDirpath, testCase)); - runValidSszTest(sszTypeNoUint, testData); - }); - } + const testData = parseSszStaticTestcase(path.join(testSuiteDirpath, testCase)); + runValidSszTest(sszTypeNoUint, testData); + }); } }; From 4ab68ae4f313f0a4b7ea6c108122155d90e3e6a1 Mon Sep 17 00:00:00 2001 From: g11tech Date: Sat, 25 May 2024 13:45:36 +0530 Subject: [PATCH 47/51] chore: types and lint fixes (#6819) --- .../src/chain/validation/aggregateAndProof.ts | 4 +--- .../perf/chain/validation/attestation.test.ts | 2 +- .../attestation/validateAttestation.test.ts | 24 +++++++++---------- .../validator/src/services/attestation.ts | 2 +- .../validator/src/services/validatorStore.ts | 5 +--- 5 files changed, 16 insertions(+), 21 deletions(-) diff --git a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts index 782cfd070390..f5a7c2ddd875 100644 --- a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts @@ -9,9 +9,7 @@ import { import {IBeaconChain} from ".."; import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/index.js"; import {RegenCaller} from "../regen/index.js"; -import { - getSeenAttDataKeyFromSignedAggregateAndProof, -} from "../../util/sszBytes.js"; +import {getSeenAttDataKeyFromSignedAggregateAndProof} from "../../util/sszBytes.js"; import {getSelectionProofSignatureSet, getAggregateAndProofSignatureSet} from "./signatureSets/index.js"; import { getAttestationDataSigningRoot, diff --git a/packages/beacon-node/test/perf/chain/validation/attestation.test.ts b/packages/beacon-node/test/perf/chain/validation/attestation.test.ts index 8f462609c063..e0e0a1e51169 100644 --- a/packages/beacon-node/test/perf/chain/validation/attestation.test.ts +++ b/packages/beacon-node/test/perf/chain/validation/attestation.test.ts @@ -42,7 +42,7 @@ describe("validate gossip attestation", () => { attestation: null, serializedData, attSlot, - seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), + seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), }, subnet0 ); diff --git a/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts index c7d9dc775c85..4a1c3badae50 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts @@ -52,7 +52,7 @@ describe("validateAttestation", () => { const {chain, subnet} = getValidData(); await expectGossipError( chain, - {attestation: null, serializedData: Buffer.alloc(0), attSlot: 0, seenAttestationDataKey: "invalid"}, + {attestation: null, serializedData: Buffer.alloc(0), attSlot: 0, seenAttestationKey: "invalid"}, subnet, GossipErrorCode.INVALID_SERIALIZED_BYTES_ERROR_CODE ); @@ -72,7 +72,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), + seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.BAD_TARGET_EPOCH @@ -91,7 +91,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), + seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.PAST_SLOT @@ -110,7 +110,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), + seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.FUTURE_SLOT @@ -135,7 +135,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), + seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET @@ -155,7 +155,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), + seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET @@ -179,7 +179,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), + seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT @@ -199,7 +199,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), + seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.INVALID_TARGET_ROOT @@ -226,7 +226,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), + seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS @@ -245,7 +245,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), + seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), }, invalidSubnet, AttestationErrorCode.INVALID_SUBNET_ID @@ -265,7 +265,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), + seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.ATTESTATION_ALREADY_KNOWN @@ -287,7 +287,7 @@ describe("validateAttestation", () => { attestation: null, serializedData, attSlot: attestation.data.slot, - seenAttestationDataKey: getSeenAttDataKeyPhase0(serializedData), + seenAttestationKey: getSeenAttDataKeyPhase0(serializedData), }, subnet, AttestationErrorCode.INVALID_SIGNATURE diff --git a/packages/validator/src/services/attestation.ts b/packages/validator/src/services/attestation.ts index e6ea81d1b1d6..8fba3306db65 100644 --- a/packages/validator/src/services/attestation.ts +++ b/packages/validator/src/services/attestation.ts @@ -1,5 +1,5 @@ import {toHexString} from "@chainsafe/ssz"; -import {allForks, BLSSignature, electra, isElectraAttestation, phase0, Slot, ssz} from "@lodestar/types"; +import {allForks, BLSSignature, phase0, Slot, ssz} from "@lodestar/types"; import {computeEpochAtSlot, isAggregatorFromCommitteeLength} from "@lodestar/state-transition"; import {sleep} from "@lodestar/utils"; import {ApiClient, routes} from "@lodestar/api"; diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index f36032004aa8..f9b9123c42cc 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -28,7 +28,6 @@ import { bellatrix, BLSPubkey, BLSSignature, - electra, Epoch, phase0, Root, @@ -803,9 +802,7 @@ export class ValidatorStore { ); } if (isAfterElectra && data.index !== 0) { - throw Error( - `Non-zero committee index post-electra during signing: att.committeeIndex ${data.index}` - ); + throw Error(`Non-zero committee index post-electra during signing: att.committeeIndex ${data.index}`); } if (this.config.getForkSeq(duty.slot) >= ForkSeq.electra && data.index !== 0) { throw Error(`Attestataion data index must be 0 post electra: index ${data.index}`); From 03da48708e0df7b1c3471326da335214d57bd21c Mon Sep 17 00:00:00 2001 From: NC Date: Sat, 8 Jun 2024 12:46:01 +0300 Subject: [PATCH 48/51] feat: add engine_getPayloadBodiesByHash and ByRange V2 (#6852) * Add ByHash and ByRange V2 * Fix build issue * Fix CI error --- packages/beacon-node/src/chain/chain.ts | 2 +- .../beacon-node/src/eth1/eth1DepositDataTracker.ts | 4 ++-- packages/beacon-node/src/execution/engine/http.ts | 9 ++++++--- .../beacon-node/src/execution/engine/interface.ts | 4 ++-- packages/beacon-node/src/execution/engine/mock.ts | 2 ++ packages/beacon-node/src/execution/engine/types.ts | 10 +++++++--- .../beacon-node/test/sim/electra-interop.test.ts | 10 +++++----- .../test/spec/presets/operations.test.ts | 2 +- .../test/unit/executionEngine/http.test.ts | 4 ++-- packages/beacon-node/test/utils/state.ts | 2 +- packages/light-client/src/spec/utils.ts | 6 +++--- .../src/block/processDepositRequest.ts | 4 ++-- .../src/block/processOperations.ts | 2 +- .../src/slot/upgradeStateToElectra.ts | 14 +++++++------- packages/state-transition/src/util/deposit.ts | 4 ++-- packages/state-transition/src/util/execution.ts | 4 ++-- packages/state-transition/src/util/genesis.ts | 2 +- .../test/unit/util/deposit.test.ts | 4 ++-- packages/types/src/electra/sszTypes.ts | 12 ++++++------ packages/types/src/electra/types.ts | 4 ++-- 20 files changed, 57 insertions(+), 48 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 25b72c57f769..f8007d12aa8b 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -1093,7 +1093,7 @@ export class BeaconChain implements IBeaconChain { // Will resolve this later // if (cpEpoch >= (this.config.ELECTRA_FORK_EPOCH ?? Infinity)) { // // finalizedState can be safely casted to Electra state since cp is already post-Electra - // if (finalizedState.eth1DepositIndex >= (finalizedState as CachedBeaconStateElectra).depositReceiptsStartIndex) { + // if (finalizedState.eth1DepositIndex >= (finalizedState as CachedBeaconStateElectra).depositRequestsStartIndex) { // // Signal eth1 to stop polling eth1Data // this.eth1.stopPollingEth1Data(); // } diff --git a/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts b/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts index c0b3ab35a73a..d0578718f29f 100644 --- a/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts +++ b/packages/beacon-node/src/eth1/eth1DepositDataTracker.ts @@ -129,9 +129,9 @@ export class Eth1DepositDataTracker { async getEth1DataAndDeposits(state: CachedBeaconStateAllForks): Promise { if ( state.epochCtx.isAfterElectra() && - state.eth1DepositIndex >= (state as CachedBeaconStateElectra).depositReceiptsStartIndex + state.eth1DepositIndex >= (state as CachedBeaconStateElectra).depositRequestsStartIndex ) { - // No need to poll eth1Data since Electra deprecates the mechanism after depositReceiptsStartIndex is reached + // No need to poll eth1Data since Electra deprecates the mechanism after depositRequestsStartIndex is reached return {eth1Data: state.eth1Data, deposits: []}; } const eth1Data = this.forcedEth1DataVote ?? (await this.getEth1Data(state)); diff --git a/packages/beacon-node/src/execution/engine/http.ts b/packages/beacon-node/src/execution/engine/http.ts index be91d4509236..5461236866c5 100644 --- a/packages/beacon-node/src/execution/engine/http.ts +++ b/packages/beacon-node/src/execution/engine/http.ts @@ -396,8 +396,9 @@ export class ExecutionEngineHttp implements IExecutionEngine { this.payloadIdCache.prune(); } - async getPayloadBodiesByHash(blockHashes: RootHex[]): Promise<(ExecutionPayloadBody | null)[]> { - const method = "engine_getPayloadBodiesByHashV1"; + async getPayloadBodiesByHash(fork: ForkName, blockHashes: RootHex[]): Promise<(ExecutionPayloadBody | null)[]> { + const method = + ForkSeq[fork] >= ForkSeq.electra ? "engine_getPayloadBodiesByHashV2" : "engine_getPayloadBodiesByHashV1"; assertReqSizeLimit(blockHashes.length, 32); const response = await this.rpc.fetchWithRetries< EngineApiRpcReturnTypes[typeof method], @@ -407,10 +408,12 @@ export class ExecutionEngineHttp implements IExecutionEngine { } async getPayloadBodiesByRange( + fork: ForkName, startBlockNumber: number, blockCount: number ): Promise<(ExecutionPayloadBody | null)[]> { - const method = "engine_getPayloadBodiesByRangeV1"; + const method = + ForkSeq[fork] >= ForkSeq.electra ? "engine_getPayloadBodiesByRangeV2" : "engine_getPayloadBodiesByRangeV1"; assertReqSizeLimit(blockCount, 32); const start = numToQuantity(startBlockNumber); const count = numToQuantity(blockCount); diff --git a/packages/beacon-node/src/execution/engine/interface.ts b/packages/beacon-node/src/execution/engine/interface.ts index 0ffeeb589df2..cb7ec7323e7d 100644 --- a/packages/beacon-node/src/execution/engine/interface.ts +++ b/packages/beacon-node/src/execution/engine/interface.ts @@ -143,9 +143,9 @@ export interface IExecutionEngine { shouldOverrideBuilder?: boolean; }>; - getPayloadBodiesByHash(blockHash: DATA[]): Promise<(ExecutionPayloadBody | null)[]>; + getPayloadBodiesByHash(fork: ForkName, blockHash: DATA[]): Promise<(ExecutionPayloadBody | null)[]>; - getPayloadBodiesByRange(start: number, count: number): Promise<(ExecutionPayloadBody | null)[]>; + getPayloadBodiesByRange(fork: ForkName, start: number, count: number): Promise<(ExecutionPayloadBody | null)[]>; getState(): ExecutionEngineState; } diff --git a/packages/beacon-node/src/execution/engine/mock.ts b/packages/beacon-node/src/execution/engine/mock.ts index aa15a2bc9532..eee176699845 100644 --- a/packages/beacon-node/src/execution/engine/mock.ts +++ b/packages/beacon-node/src/execution/engine/mock.ts @@ -98,7 +98,9 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { engine_getPayloadV3: this.getPayload.bind(this), engine_getPayloadV4: this.getPayload.bind(this), engine_getPayloadBodiesByHashV1: this.getPayloadBodiesByHash.bind(this), + engine_getPayloadBodiesByHashV2: this.getPayloadBodiesByHash.bind(this), engine_getPayloadBodiesByRangeV1: this.getPayloadBodiesByRange.bind(this), + engine_getPayloadBodiesByRangeV2: this.getPayloadBodiesByRange.bind(this), }; } diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index 9db6717c4749..0b6fffbafbf2 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -58,12 +58,14 @@ export type EngineApiRpcParamTypes = { * 1. Array of DATA - Array of block_hash field values of the ExecutionPayload structure * */ engine_getPayloadBodiesByHashV1: DATA[][]; + engine_getPayloadBodiesByHashV2: DATA[][]; /** * 1. start: QUANTITY, 64 bits - Starting block number * 2. count: QUANTITY, 64 bits - Number of blocks to return */ engine_getPayloadBodiesByRangeV1: [start: QUANTITY, count: QUANTITY]; + engine_getPayloadBodiesByRangeV2: [start: QUANTITY, count: QUANTITY]; }; export type PayloadStatus = { @@ -102,8 +104,10 @@ export type EngineApiRpcReturnTypes = { engine_getPayloadV4: ExecutionPayloadResponse; engine_getPayloadBodiesByHashV1: (ExecutionPayloadBodyRpc | null)[]; + engine_getPayloadBodiesByHashV2: (ExecutionPayloadBodyRpc | null)[]; engine_getPayloadBodiesByRangeV1: (ExecutionPayloadBodyRpc | null)[]; + engine_getPayloadBodiesByRangeV2: (ExecutionPayloadBodyRpc | null)[]; }; type ExecutionPayloadRpcWithValue = { @@ -217,8 +221,8 @@ export function serializeExecutionPayload(fork: ForkName, data: allForks.Executi // ELECTRA adds depositRequests/depositRequests to the ExecutionPayload if (ForkSeq[fork] >= ForkSeq.electra) { - const {depositReceipts, withdrawalRequests} = data as electra.ExecutionPayload; - payload.depositRequests = depositReceipts.map(serializeDepositRequest); + const {depositRequests, withdrawalRequests} = data as electra.ExecutionPayload; + payload.depositRequests = depositRequests.map(serializeDepositRequest); payload.withdrawalRequests = withdrawalRequests.map(serializeExecutionLayerWithdrawalRequest); } @@ -316,7 +320,7 @@ export function parseExecutionPayload( `depositRequests missing for ${fork} >= electra executionPayload number=${executionPayload.blockNumber} hash=${data.blockHash}` ); } - (executionPayload as electra.ExecutionPayload).depositReceipts = depositRequests.map(deserializeDepositRequest); + (executionPayload as electra.ExecutionPayload).depositRequests = depositRequests.map(deserializeDepositRequest); if (withdrawalRequests == null) { throw Error( diff --git a/packages/beacon-node/test/sim/electra-interop.test.ts b/packages/beacon-node/test/sim/electra-interop.test.ts index 962e0aa5dea9..2dfa5ee0f77c 100644 --- a/packages/beacon-node/test/sim/electra-interop.test.ts +++ b/packages/beacon-node/test/sim/electra-interop.test.ts @@ -219,11 +219,11 @@ describe("executionEngine / ExecutionEngineHttp", function () { } } - if (payload.depositReceipts.length !== 1) { - throw Error(`Number of depositRequests mismatched. Expected: 1, actual: ${payload.depositReceipts.length}`); + if (payload.depositRequests.length !== 1) { + throw Error(`Number of depositRequests mismatched. Expected: 1, actual: ${payload.depositRequests.length}`); } - const actualDepositRequest = payload.depositReceipts[0]; + const actualDepositRequest = payload.depositRequests[0]; assert.deepStrictEqual( actualDepositRequest, depositRequestB, @@ -431,8 +431,8 @@ describe("executionEngine / ExecutionEngineHttp", function () { throw Error("Historical validator length for epoch 1 or 2 is not dropped properly"); } - if (headState.depositReceiptsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { - throw Error("state.depositReceiptsStartIndex is not set upon processing new deposit receipt"); + if (headState.depositRequestsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { + throw Error("state.depositRequestsStartIndex is not set upon processing new deposit receipt"); } // wait for 1 slot to print current epoch stats diff --git a/packages/beacon-node/test/spec/presets/operations.test.ts b/packages/beacon-node/test/spec/presets/operations.test.ts index c6d5d8297673..7312a79cce0d 100644 --- a/packages/beacon-node/test/spec/presets/operations.test.ts +++ b/packages/beacon-node/test/spec/presets/operations.test.ts @@ -143,7 +143,7 @@ const operations: TestRunnerFn = (fork, block: ssz[fork].BeaconBlock, body: ssz[fork].BeaconBlockBody, deposit: ssz.phase0.Deposit, - deposit_receipt: ssz.electra.DepositReceipt, + deposit_receipt: ssz.electra.DepositRequest, proposer_slashing: ssz.phase0.ProposerSlashing, voluntary_exit: ssz.phase0.SignedVoluntaryExit, // Altair diff --git a/packages/beacon-node/test/unit/executionEngine/http.test.ts b/packages/beacon-node/test/unit/executionEngine/http.test.ts index 6d3cb17335e6..936a89afcf36 100644 --- a/packages/beacon-node/test/unit/executionEngine/http.test.ts +++ b/packages/beacon-node/test/unit/executionEngine/http.test.ts @@ -218,7 +218,7 @@ describe("ExecutionEngine / http", () => { returnValue = response; - const res = await executionEngine.getPayloadBodiesByHash(reqBlockHashes); + const res = await executionEngine.getPayloadBodiesByHash(ForkName.bellatrix, reqBlockHashes); expect(reqJsonRpcPayload).toEqual(request); expect(res.map(serializeExecutionPayloadBody)).toEqual(response.result); @@ -271,7 +271,7 @@ describe("ExecutionEngine / http", () => { returnValue = response; - const res = await executionEngine.getPayloadBodiesByRange(startBlockNumber, blockCount); + const res = await executionEngine.getPayloadBodiesByRange(ForkName.bellatrix, startBlockNumber, blockCount); expect(reqJsonRpcPayload).toEqual(request); expect(res.map(serializeExecutionPayloadBody)).toEqual(response.result); diff --git a/packages/beacon-node/test/utils/state.ts b/packages/beacon-node/test/utils/state.ts index 04e1dc710b5e..70b50c86004b 100644 --- a/packages/beacon-node/test/utils/state.ts +++ b/packages/beacon-node/test/utils/state.ts @@ -97,7 +97,7 @@ export function generateState( if (forkSeq >= ForkSeq.electra) { const stateElectra = state as electra.BeaconState; - stateElectra.depositReceiptsStartIndex = 2023n; + stateElectra.depositRequestsStartIndex = 2023n; stateElectra.latestExecutionPayloadHeader = ssz.electra.ExecutionPayloadHeader.defaultValue(); } diff --git a/packages/light-client/src/spec/utils.ts b/packages/light-client/src/spec/utils.ts index 0c1dccbb7038..027ef87dda48 100644 --- a/packages/light-client/src/spec/utils.ts +++ b/packages/light-client/src/spec/utils.ts @@ -106,8 +106,8 @@ export function upgradeLightClientHeader( // eslint-disable-next-line no-fallthrough case ForkName.electra: - (upgradedHeader as electra.LightClientHeader).execution.depositReceiptsRoot = - ssz.electra.LightClientHeader.fields.execution.fields.depositReceiptsRoot.defaultValue(); + (upgradedHeader as electra.LightClientHeader).execution.depositRequestsRoot = + ssz.electra.LightClientHeader.fields.execution.fields.depositRequestsRoot.defaultValue(); (upgradedHeader as electra.LightClientHeader).execution.withdrawalRequestsRoot = ssz.electra.LightClientHeader.fields.execution.fields.withdrawalRequestsRoot.defaultValue(); @@ -148,7 +148,7 @@ export function isValidLightClientHeader(config: ChainForkConfig, header: allFor if (epoch < config.ELECTRA_FORK_EPOCH) { if ( - (header as electra.LightClientHeader).execution.depositReceiptsRoot !== undefined || + (header as electra.LightClientHeader).execution.depositRequestsRoot !== undefined || (header as electra.LightClientHeader).execution.withdrawalRequestsRoot !== undefined ) { return false; diff --git a/packages/state-transition/src/block/processDepositRequest.ts b/packages/state-transition/src/block/processDepositRequest.ts index d5525f3cd544..ca6fe4206188 100644 --- a/packages/state-transition/src/block/processDepositRequest.ts +++ b/packages/state-transition/src/block/processDepositRequest.ts @@ -9,8 +9,8 @@ export function processDepositRequest( state: CachedBeaconStateElectra, depositRequest: electra.DepositRequest ): void { - if (state.depositReceiptsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { - state.depositReceiptsStartIndex = BigInt(depositRequest.index); + if (state.depositRequestsStartIndex === UNSET_DEPOSIT_RECEIPTS_START_INDEX) { + state.depositRequestsStartIndex = BigInt(depositRequest.index); } applyDeposit(fork, state, depositRequest); diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index dd6871b8b80a..f996304e9f78 100644 --- a/packages/state-transition/src/block/processOperations.ts +++ b/packages/state-transition/src/block/processOperations.ts @@ -71,7 +71,7 @@ export function processOperations( processExecutionLayerWithdrawalRequest(fork, state as CachedBeaconStateElectra, elWithdrawalRequest); } - for (const depositRequest of bodyElectra.executionPayload.depositReceipts) { + for (const depositRequest of bodyElectra.executionPayload.depositRequests) { processDepositRequest(fork, stateElectra, depositRequest); } diff --git a/packages/state-transition/src/slot/upgradeStateToElectra.ts b/packages/state-transition/src/slot/upgradeStateToElectra.ts index 72255ccddee3..3d77366cbe57 100644 --- a/packages/state-transition/src/slot/upgradeStateToElectra.ts +++ b/packages/state-transition/src/slot/upgradeStateToElectra.ts @@ -50,16 +50,16 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache stateElectraView.nextSyncCommittee = stateElectraCloned.nextSyncCommittee; stateElectraView.latestExecutionPayloadHeader = ssz.electra.BeaconState.fields.latestExecutionPayloadHeader.toViewDU({ ...stateElectraCloned.latestExecutionPayloadHeader.toValue(), - depositReceiptsRoot: ssz.Root.defaultValue(), + depositRequestsRoot: ssz.Root.defaultValue(), withdrawalRequestsRoot: ssz.Root.defaultValue(), }); stateElectraView.nextWithdrawalIndex = stateDeneb.nextWithdrawalIndex; stateElectraView.nextWithdrawalValidatorIndex = stateDeneb.nextWithdrawalValidatorIndex; stateElectraView.historicalSummaries = stateElectraCloned.historicalSummaries; - // latestExecutionPayloadHeader's depositReceiptsRoot and withdrawalRequestsRoot set to zeros by default - // default value of depositReceiptsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX - stateElectraView.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + // latestExecutionPayloadHeader's depositRequestsRoot and withdrawalRequestsRoot set to zeros by default + // default value of depositRequestsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX + stateElectraView.depositRequestsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; stateElectraView.depositBalanceToConsume = BigInt(0); stateElectraView.exitBalanceToConsume = BigInt(0); @@ -136,9 +136,9 @@ export function upgradeStateToElectraOriginal(stateDeneb: CachedBeaconStateDeneb epoch: stateDeneb.epochCtx.epoch, }); - // latestExecutionPayloadHeader's depositReceiptsRoot and withdrawalRequestsRoot set to zeros by default - // default value of depositReceiptsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX - stateElectra.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + // latestExecutionPayloadHeader's depositRequestsRoot and withdrawalRequestsRoot set to zeros by default + // default value of depositRequestsStartIndex is UNSET_DEPOSIT_RECEIPTS_START_INDEX + stateElectra.depositRequestsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; const validatorsArr = stateElectra.validators.getAllReadonly(); diff --git a/packages/state-transition/src/util/deposit.ts b/packages/state-transition/src/util/deposit.ts index 493099fdd982..e8ef93c515d2 100644 --- a/packages/state-transition/src/util/deposit.ts +++ b/packages/state-transition/src/util/deposit.ts @@ -9,9 +9,9 @@ export function getEth1DepositCount(state: CachedBeaconStateAllForks, eth1Data?: // eth1DataIndexLimit = min(UintNum64, UintBn64) can be safely casted as UintNum64 // since the result lies within upper and lower bound of UintNum64 const eth1DataIndexLimit: UintNum64 = - eth1DataToUse.depositCount < electraState.depositReceiptsStartIndex + eth1DataToUse.depositCount < electraState.depositRequestsStartIndex ? eth1DataToUse.depositCount - : Number(electraState.depositReceiptsStartIndex); + : Number(electraState.depositRequestsStartIndex); if (state.eth1DepositIndex < eth1DataIndexLimit) { return Math.min(MAX_DEPOSITS, eth1DataIndexLimit - state.eth1DepositIndex); diff --git a/packages/state-transition/src/util/execution.ts b/packages/state-transition/src/util/execution.ts index 2789836d0e3b..6939c78416b6 100644 --- a/packages/state-transition/src/util/execution.ts +++ b/packages/state-transition/src/util/execution.ts @@ -171,8 +171,8 @@ 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).depositRequestsRoot = + ssz.electra.DepositRequests.hashTreeRoot((payload as electra.ExecutionPayload).depositRequests); (bellatrixPayloadFields as electra.ExecutionPayloadHeader).withdrawalRequestsRoot = ssz.electra.ExecutionLayerWithdrawalRequests.hashTreeRoot( (payload as electra.ExecutionPayload).withdrawalRequests diff --git a/packages/state-transition/src/util/genesis.ts b/packages/state-transition/src/util/genesis.ts index e1003dca1946..ca894fd5b4e5 100644 --- a/packages/state-transition/src/util/genesis.ts +++ b/packages/state-transition/src/util/genesis.ts @@ -308,7 +308,7 @@ export function initializeBeaconStateFromEth1( stateElectra.latestExecutionPayloadHeader = (executionPayloadHeader as CompositeViewDU) ?? ssz.electra.ExecutionPayloadHeader.defaultViewDU(); - stateElectra.depositReceiptsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; + stateElectra.depositRequestsStartIndex = UNSET_DEPOSIT_RECEIPTS_START_INDEX; } state.commit(); diff --git a/packages/state-transition/test/unit/util/deposit.test.ts b/packages/state-transition/test/unit/util/deposit.test.ts index e8f7f7a86af8..677a15724ece 100644 --- a/packages/state-transition/test/unit/util/deposit.test.ts +++ b/packages/state-transition/test/unit/util/deposit.test.ts @@ -43,7 +43,7 @@ describe("getEth1DepositCount", () => { throw Error("Not a post-Electra state"); } - postElectraState.depositReceiptsStartIndex = 1000n; + postElectraState.depositRequestsStartIndex = 1000n; postElectraState.eth1Data.depositCount = 995; // 1. Should get less than MAX_DEPOSIT @@ -77,7 +77,7 @@ describe("getEth1DepositCount", () => { throw Error("Not a post-Electra state"); } - postElectraState.depositReceiptsStartIndex = 1000n; + postElectraState.depositRequestsStartIndex = 1000n; postElectraState.eth1Data.depositCount = 1005; // Before eth1DepositIndex reaching the start index diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index a8249156c7b9..b03be1697fd1 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -112,7 +112,7 @@ export const SignedAggregateAndProof = new ContainerType( {typeName: "SignedAggregateAndProof", jsonCase: "eth2"} ); -export const DepositReceipt = new ContainerType( +export const DepositRequest = new ContainerType( { pubkey: BLSPubkey, withdrawalCredentials: Bytes32, @@ -120,10 +120,10 @@ export const DepositReceipt = new ContainerType( signature: BLSSignature, index: DepositIndex, }, - {typeName: "DepositReceipt", jsonCase: "eth2"} + {typeName: "DepositRequest", jsonCase: "eth2"} ); -export const DepositReceipts = new ListCompositeType(DepositReceipt, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD); +export const DepositRequests = new ListCompositeType(DepositRequest, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD); export const ExecutionLayerWithdrawalRequest = new ContainerType( { @@ -141,7 +141,7 @@ export const ExecutionLayerWithdrawalRequests = new ListCompositeType( export const ExecutionPayload = new ContainerType( { ...denebSsz.ExecutionPayload.fields, - depositReceipts: DepositReceipts, // New in ELECTRA + depositRequests: DepositRequests, // New in ELECTRA withdrawalRequests: ExecutionLayerWithdrawalRequests, // New in ELECTRA }, {typeName: "ExecutionPayload", jsonCase: "eth2"} @@ -150,7 +150,7 @@ export const ExecutionPayload = new ContainerType( export const ExecutionPayloadHeader = new ContainerType( { ...denebSsz.ExecutionPayloadHeader.fields, - depositReceiptsRoot: Root, // New in ELECTRA + depositRequestsRoot: Root, // New in ELECTRA withdrawalRequestsRoot: Root, // New in ELECTRA }, {typeName: "ExecutionPayloadHeader", jsonCase: "eth2"} @@ -340,7 +340,7 @@ export const BeaconState = new ContainerType( nextWithdrawalValidatorIndex: capellaSsz.BeaconState.fields.nextWithdrawalValidatorIndex, // Deep history valid from Capella onwards historicalSummaries: capellaSsz.BeaconState.fields.historicalSummaries, - depositReceiptsStartIndex: UintBn64, // New in ELECTRA:EIP6110 + depositRequestsStartIndex: UintBn64, // New in ELECTRA:EIP6110 depositBalanceToConsume: Gwei, // New in Electra:EIP7251 exitBalanceToConsume: Gwei, // New in Electra:EIP7251 earliestExitEpoch: Epoch, // New in Electra:EIP7251 diff --git a/packages/types/src/electra/types.ts b/packages/types/src/electra/types.ts index fc95ecde562d..994e5a732243 100644 --- a/packages/types/src/electra/types.ts +++ b/packages/types/src/electra/types.ts @@ -9,8 +9,8 @@ export type AttesterSlashing = ValueOf; export type AggregateAndProof = ValueOf; export type SignedAggregateAndProof = ValueOf; -export type DepositRequest = ValueOf; -export type DepositRequests = ValueOf; +export type DepositRequest = ValueOf; +export type DepositRequests = ValueOf; export type ExecutionLayerWithdrawalRequest = ValueOf; export type ExecutionLayerWithdrawalRequests = ValueOf; From 24766986577f6cde9a9d902a141be25ee10af8e5 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Wed, 12 Jun 2024 09:22:40 +0100 Subject: [PATCH 49/51] fix: align WithdrawalRequestV1 field names with latest spec (#6877) --- packages/beacon-node/src/execution/engine/payloadIdCache.ts | 2 +- packages/beacon-node/src/execution/engine/types.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/beacon-node/src/execution/engine/payloadIdCache.ts b/packages/beacon-node/src/execution/engine/payloadIdCache.ts index 35a50e97cfb3..f79c28582459 100644 --- a/packages/beacon-node/src/execution/engine/payloadIdCache.ts +++ b/packages/beacon-node/src/execution/engine/payloadIdCache.ts @@ -28,7 +28,7 @@ export type DepositRequestV1 = { export type ExecutionLayerWithdrawalRequestV1 = { sourceAddress: DATA; - validatorPublicKey: DATA; + validatorPubkey: DATA; amount: QUANTITY; }; diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index 0b6fffbafbf2..675b64062583 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -423,7 +423,7 @@ export function serializeExecutionLayerWithdrawalRequest( ): ExecutionLayerWithdrawalRequestRpc { return { sourceAddress: bytesToData(withdrawalRequest.sourceAddress), - validatorPublicKey: bytesToData(withdrawalRequest.validatorPubkey), + validatorPubkey: bytesToData(withdrawalRequest.validatorPubkey), amount: numToQuantity(withdrawalRequest.amount), }; } @@ -433,7 +433,7 @@ export function deserializeExecutionLayerWithdrawalRequest( ): electra.ExecutionLayerWithdrawalRequest { return { sourceAddress: dataToBytes(withdrawalRequest.sourceAddress, 20), - validatorPubkey: dataToBytes(withdrawalRequest.validatorPublicKey, 48), + validatorPubkey: dataToBytes(withdrawalRequest.validatorPubkey, 48), amount: quantityToNum(withdrawalRequest.amount), }; } From 4239f722fa10fe997f28749bdc8a7cbdcda804b1 Mon Sep 17 00:00:00 2001 From: NC Date: Fri, 14 Jun 2024 14:11:23 +0300 Subject: [PATCH 50/51] feat: move attestation committee at the end of attestation (#6883) --- .../src/chain/validation/attestation.ts | 2 +- packages/beacon-node/src/util/sszBytes.ts | 26 ++++++++++--------- .../test/unit/util/sszBytes.test.ts | 8 +++--- packages/types/src/electra/sszTypes.ts | 2 +- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index 0141eaafbb06..c44a8b7913c9 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -456,7 +456,7 @@ async function validateGossipAttestationNoSignatureCheck( let attDataRootHex: RootHex; const signature = attestationOrCache.attestation ? attestationOrCache.attestation.signature - : getSignatureFromAttestationSerialized(fork, attestationOrCache.serializedData); + : getSignatureFromAttestationSerialized(attestationOrCache.serializedData); if (signature === null) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.INVALID_SERIALIZED_BYTES, diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index b3f1c4d9432b..f88cc129ff3b 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -26,8 +26,8 @@ export type AttDataCommitteeBitsBase64 = string; // class Attestation(Container): // aggregation_bits: BitList[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] - offset 4 // data: AttestationData - target data - 128 -// committee_bits: BitVector[MAX_COMMITTEES_PER_SLOT] // signature: BLSSignature - 96 +// committee_bits: BitVector[MAX_COMMITTEES_PER_SLOT] // // for all forks // class AttestationData(Container): 128 bytes fixed size @@ -82,14 +82,19 @@ export function getSeenAttDataKey(forkSeq: ForkSeq, data: Uint8Array): SeenAttDa * Return null if data is not long enough to extract attestation data. */ export function getSeenAttDataKeyElectra(electraAttestationBytes: Uint8Array): AttDataCommitteeBitsBase64 | null { - const startIndex = VARIABLE_FIELD_OFFSET; - const seenKeyLength = ATTESTATION_DATA_SIZE + COMMITTEE_BITS_SIZE; + const attestationData = getSeenAttDataKeyPhase0(electraAttestationBytes); - if (electraAttestationBytes.length < startIndex + seenKeyLength) { + if (attestationData === null) { return null; } - return toBase64(electraAttestationBytes.subarray(startIndex, startIndex + seenKeyLength)); + const committeeBits = getCommitteeBitsFromAttestationSerialized(electraAttestationBytes); + + if (committeeBits === null) { + return null; + } + + return attestationData + toBase64(committeeBits.uint8Array); } /** @@ -112,7 +117,7 @@ export function getSeenAttDataKeyPhase0(data: Uint8Array): AttDataBase64 | null export function getAggregationBitsFromAttestationSerialized(fork: ForkName, data: Uint8Array): BitArray | null { const aggregationBitsStartIndex = ForkSeq[fork] >= ForkSeq.electra - ? VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + COMMITTEE_BITS_SIZE + SIGNATURE_SIZE + ? VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE + COMMITTEE_BITS_SIZE : VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE; if (data.length < aggregationBitsStartIndex) { @@ -127,11 +132,8 @@ export function getAggregationBitsFromAttestationSerialized(fork: ForkName, data * Extract signature from attestation serialized bytes. * Return null if data is not long enough to extract signature. */ -export function getSignatureFromAttestationSerialized(fork: ForkName, data: Uint8Array): BLSSignature | null { - const signatureStartIndex = - ForkSeq[fork] >= ForkSeq.electra - ? VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + COMMITTEE_BITS_SIZE - : VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE; +export function getSignatureFromAttestationSerialized(data: Uint8Array): BLSSignature | null { + const signatureStartIndex = VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE; if (data.length < signatureStartIndex + SIGNATURE_SIZE) { return null; @@ -145,7 +147,7 @@ export function getSignatureFromAttestationSerialized(fork: ForkName, data: Uint * Return null if data is not long enough to extract committee bits. */ export function getCommitteeBitsFromAttestationSerialized(data: Uint8Array): BitArray | null { - const committeeBitsStartIndex = VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE; + const committeeBitsStartIndex = VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE; if (data.length < committeeBitsStartIndex + COMMITTEE_BITS_SIZE) { return null; diff --git a/packages/beacon-node/test/unit/util/sszBytes.test.ts b/packages/beacon-node/test/unit/util/sszBytes.test.ts index 03e4a7f7a71d..5edd9fb96fb1 100644 --- a/packages/beacon-node/test/unit/util/sszBytes.test.ts +++ b/packages/beacon-node/test/unit/util/sszBytes.test.ts @@ -54,12 +54,12 @@ describe("attestation SSZ serialized picking", () => { attestation.aggregationBits.toBoolArray() ); expect(getCommitteeBitsFromAttestationSerialized(bytes)).toEqual(attestation.committeeBits); - expect(getSignatureFromAttestationSerialized(ForkName.electra, bytes)).toEqual(attestation.signature); + expect(getSignatureFromAttestationSerialized(bytes)).toEqual(attestation.signature); } else { expect(getAggregationBitsFromAttestationSerialized(ForkName.phase0, bytes)?.toBoolArray()).toEqual( attestation.aggregationBits.toBoolArray() ); - expect(getSignatureFromAttestationSerialized(ForkName.phase0, bytes)).toEqual(attestation.signature); + expect(getSignatureFromAttestationSerialized(bytes)).toEqual(attestation.signature); } const attDataBase64 = ssz.phase0.AttestationData.serialize(attestation.data); @@ -99,8 +99,8 @@ describe("attestation SSZ serialized picking", () => { it("getSignatureFromAttestationSerialized - invalid data", () => { const invalidSignatureDataSizes = [0, 4, 100, 128, 227]; for (const size of invalidSignatureDataSizes) { - expect(getSignatureFromAttestationSerialized(ForkName.phase0, Buffer.alloc(size))).toBeNull(); - expect(getSignatureFromAttestationSerialized(ForkName.electra, Buffer.alloc(size))).toBeNull(); + expect(getSignatureFromAttestationSerialized(Buffer.alloc(size))).toBeNull(); + expect(getSignatureFromAttestationSerialized(Buffer.alloc(size))).toBeNull(); } }); }); diff --git a/packages/types/src/electra/sszTypes.ts b/packages/types/src/electra/sszTypes.ts index b03be1697fd1..ab9cbbaacd80 100644 --- a/packages/types/src/electra/sszTypes.ts +++ b/packages/types/src/electra/sszTypes.ts @@ -62,8 +62,8 @@ export const Attestation = new ContainerType( { aggregationBits: AggregationBits, // Modified in ELECTRA data: phase0Ssz.AttestationData, - committeeBits: CommitteeBits, // New in ELECTRA signature: BLSSignature, + committeeBits: CommitteeBits, // New in ELECTRA }, {typeName: "Attestation", jsonCase: "eth2"} ); From 4f800e5e3e7e9eaa1bc7b6fcfb1a9d3dbb84521b Mon Sep 17 00:00:00 2001 From: harkamal Date: Wed, 19 Jun 2024 19:27:59 +0530 Subject: [PATCH 51/51] rebase fixes chore: fix bls and blst versioning fix: add ForkName.electra to ForkBlobsInfo some api header lookup fixes more api fixes make the api data safe Co-authored-by: matthewkeil --- packages/api/src/beacon/routes/beacon/pool.ts | 11 ++++++++-- packages/api/src/beacon/routes/validator.ts | 21 ++++++++++++++----- .../api/test/unit/beacon/testData/beacon.ts | 2 +- .../test/unit/beacon/testData/validator.ts | 2 +- .../beacon-node/src/chain/blocks/types.ts | 2 +- .../test/unit/webEsmBundle.browser.test.ts | 4 ++-- packages/state-transition/package.json | 4 ++-- packages/validator/package.json | 2 +- .../test/unit/services/attestation.test.ts | 5 ++++- yarn.lock | 14 +++++++++++++ 10 files changed, 51 insertions(+), 16 deletions(-) diff --git a/packages/api/src/beacon/routes/beacon/pool.ts b/packages/api/src/beacon/routes/beacon/pool.ts index 3c390aa41481..499e8af432d7 100644 --- a/packages/api/src/beacon/routes/beacon/pool.ts +++ b/packages/api/src/beacon/routes/beacon/pool.ts @@ -17,6 +17,7 @@ import { } from "../../../utils/codecs.js"; import {MetaHeader, VersionCodec, VersionMeta} from "../../../utils/metadata.js"; import {toForkName} from "../../../utils/fork.js"; +import {fromHeaders} from "../../../utils/headers.js"; // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes @@ -247,7 +248,12 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { - const fork = toForkName(headers[MetaHeader.Version]); + const versionHeader = fromHeaders(headers, MetaHeader.Version, false); + const fork = + versionHeader !== undefined + ? toForkName(versionHeader) + : config.getForkName(Number((body as {data: {slot: string}}[])[0]?.data.slot ?? 0)); + return { signedAttestations: ForkSeq[fork] >= ForkSeq.electra @@ -266,7 +272,8 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { - const fork = toForkName(headers[MetaHeader.Version]); + const versionHeader = fromHeaders(headers, MetaHeader.Version, true); + const fork = toForkName(versionHeader); return { signedAttestations: ForkSeq[fork] >= ForkSeq.electra diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index 56c23ceb4866..5be4a03800f9 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -40,6 +40,7 @@ import { VersionMeta, VersionType, } from "../../utils/metadata.js"; +import {fromHeaders} from "../../utils/headers.js"; // See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes @@ -856,7 +857,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({ attestationDataRoot: fromHexString(query.attestation_data_root), slot: query.slot, - committeeIndex: query.slot, + committeeIndex: query.committeeIndex, }), schema: { query: { @@ -878,7 +879,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { - const fork = config.getForkName(signedAggregateAndProofs[0].message.aggregate.data.slot); + const fork = config.getForkName(signedAggregateAndProofs[0]?.message.aggregate.data.slot ?? 0); return { body: ForkSeq[fork] >= ForkSeq.electra @@ -892,7 +893,16 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { - const fork = toForkName(headers[MetaHeader.Version]); + const versionHeader = fromHeaders(headers, MetaHeader.Version, false); + const fork = + versionHeader !== undefined + ? toForkName(versionHeader) + : config.getForkName( + Number( + (body as {message: {aggregate: {data: {slot: string}}}}[])[0]?.message.aggregate.data.slot ?? 0 + ) + ); + return { signedAggregateAndProofs: ForkSeq[fork] >= ForkSeq.electra @@ -901,7 +911,7 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { - const fork = config.getForkName(signedAggregateAndProofs[0].message.aggregate.data.slot); + const fork = config.getForkName(signedAggregateAndProofs[0]?.message.aggregate.data.slot ?? 0); return { body: ForkSeq[fork] >= ForkSeq.electra @@ -915,7 +925,8 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { - const fork = toForkName(headers[MetaHeader.Version]); + const versionHeader = fromHeaders(headers, MetaHeader.Version, true); + const fork = toForkName(versionHeader); return { signedAggregateAndProofs: ForkSeq[fork] >= ForkSeq.electra diff --git a/packages/api/test/unit/beacon/testData/beacon.ts b/packages/api/test/unit/beacon/testData/beacon.ts index 9dad5f0079a2..be381f44b7de 100644 --- a/packages/api/test/unit/beacon/testData/beacon.ts +++ b/packages/api/test/unit/beacon/testData/beacon.ts @@ -89,7 +89,7 @@ export const testData: GenericServerTestCases = { getPoolAttestations: { args: {slot: 1, committeeIndex: 2}, - res: {data: [ssz.phase0.Attestation.defaultValue()]}, + res: {data: [ssz.phase0.Attestation.defaultValue()], meta: {version: ForkName.deneb}}, }, getPoolAttesterSlashings: { args: undefined, diff --git a/packages/api/test/unit/beacon/testData/validator.ts b/packages/api/test/unit/beacon/testData/validator.ts index 01783241bd2b..8ba02ee1435a 100644 --- a/packages/api/test/unit/beacon/testData/validator.ts +++ b/packages/api/test/unit/beacon/testData/validator.ts @@ -103,7 +103,7 @@ export const testData: GenericServerTestCases = { res: {data: ssz.altair.SyncCommitteeContribution.defaultValue()}, }, getAggregatedAttestation: { - args: {attestationDataRoot: ZERO_HASH, slot: 32000, index: 2}, + args: {attestationDataRoot: ZERO_HASH, slot: 32000, committeeIndex: 2}, res: {data: ssz.phase0.Attestation.defaultValue(), meta: {version: ForkName.phase0}}, }, publishAggregateAndProofs: { diff --git a/packages/beacon-node/src/chain/blocks/types.ts b/packages/beacon-node/src/chain/blocks/types.ts index 2996bac7887f..6368f91e9197 100644 --- a/packages/beacon-node/src/chain/blocks/types.ts +++ b/packages/beacon-node/src/chain/blocks/types.ts @@ -36,7 +36,7 @@ export enum GossipedInputType { type BlobsCacheMap = Map; -type ForkBlobsInfo = {fork: ForkName.deneb}; +type ForkBlobsInfo = {fork: ForkName.deneb | ForkName.electra}; type BlobsData = {blobs: deneb.BlobSidecars; blobsBytes: (Uint8Array | null)[]; blobsSource: BlobsSource}; export type BlockInputDataBlobs = ForkBlobsInfo & BlobsData; export type BlockInputData = BlockInputDataBlobs; diff --git a/packages/light-client/test/unit/webEsmBundle.browser.test.ts b/packages/light-client/test/unit/webEsmBundle.browser.test.ts index defc421d7071..49b0f877d46c 100644 --- a/packages/light-client/test/unit/webEsmBundle.browser.test.ts +++ b/packages/light-client/test/unit/webEsmBundle.browser.test.ts @@ -1,7 +1,7 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access */ import {expect, describe, it, vi, beforeAll} from "vitest"; import {sleep} from "@lodestar/utils"; -import {Lightclient, LightclientEvent, utils, transport} from "../../dist/lightclient.min.mjs"; +import {Lightclient, LightclientEvent, utils, transport} from "../../src/index.js"; describe("web bundle for lightclient", () => { vi.setConfig({testTimeout: 20_000}); diff --git a/packages/state-transition/package.json b/packages/state-transition/package.json index 3a27cc8986c2..10041b6080e0 100644 --- a/packages/state-transition/package.json +++ b/packages/state-transition/package.json @@ -59,8 +59,8 @@ "types": "lib/index.d.ts", "dependencies": { "@chainsafe/as-sha256": "^0.4.1", - "@chainsafe/bls": "^8.1.0", - "@chainsafe/blst": "^1.0.0", + "@chainsafe/bls": "7.1.3", + "@chainsafe/blst": "^0.2.11", "@chainsafe/persistent-merkle-tree": "^0.7.1", "@chainsafe/ssz": "^0.16.0", "@lodestar/config": "^1.19.0", diff --git a/packages/validator/package.json b/packages/validator/package.json index 42df23ae5d9d..3bac2810e037 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -45,7 +45,7 @@ "blockchain" ], "dependencies": { - "@chainsafe/bls": "^8.1.0", + "@chainsafe/bls": "7.1.3", "@chainsafe/ssz": "^0.16.0", "@lodestar/api": "^1.19.0", "@lodestar/config": "^1.19.0", diff --git a/packages/validator/test/unit/services/attestation.test.ts b/packages/validator/test/unit/services/attestation.test.ts index 9227af9c8e08..f71dd4715a60 100644 --- a/packages/validator/test/unit/services/attestation.test.ts +++ b/packages/validator/test/unit/services/attestation.test.ts @@ -5,6 +5,7 @@ import {ssz} from "@lodestar/types"; import {routes} from "@lodestar/api"; import {createChainForkConfig} from "@lodestar/config"; import {config} from "@lodestar/config/default"; +import {ForkName} from "@lodestar/params"; import {AttestationService, AttestationServiceOpts} from "../../../src/services/attestation.js"; import {AttDutyAndProof} from "../../../src/services/attestationDuties.js"; import {ValidatorStore} from "../../../src/services/validatorStore.js"; @@ -100,7 +101,9 @@ describe("AttestationService", function () { // Mock beacon's attestation and aggregates endpoints api.validator.produceAttestationData.mockResolvedValue(mockApiResponse({data: attestation.data})); - api.validator.getAggregatedAttestation.mockResolvedValue(mockApiResponse({data: attestation})); + api.validator.getAggregatedAttestation.mockResolvedValue( + mockApiResponse({data: attestation, meta: {version: ForkName.phase0}}) + ); api.beacon.submitPoolAttestations.mockResolvedValue(mockApiResponse({})); api.validator.publishAggregateAndProofs.mockResolvedValue(mockApiResponse({})); diff --git a/yarn.lock b/yarn.lock index e70da22818ce..4a7675f3a837 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2849,6 +2849,13 @@ resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.38.tgz#a81fd8606d481f873a3800c6ebae4f1d768a56a9" integrity sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA== +"@types/buffer-xor@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/buffer-xor/-/buffer-xor-2.0.2.tgz#d8c463583b8fbb322ea824562dc78a0c3cea2ca6" + integrity sha512-OqdCua7QCTupPnJgmyGJUpxWgbuOi0IMIVslXTSePS2o+qDrDB6f2Pg44zRyqhUA5GbFAf39U8z0+mH4WG0fLQ== + dependencies: + "@types/node" "*" + "@types/cacheable-request@^6.0.1": version "6.0.3" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" @@ -4435,6 +4442,13 @@ buffer-xor@^1.0.3: resolved "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= +buffer-xor@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-2.0.2.tgz#34f7c64f04c777a1f8aac5e661273bb9dd320289" + integrity sha512-eHslX0bin3GB+Lx2p7lEYRShRewuNZL3fUl4qlVJGGiwoPGftmt8JQgk2Y9Ji5/01TnVDo33E5b5O3vUB1HdqQ== + dependencies: + safe-buffer "^5.1.1" + buffer@4.9.2, buffer@^4.3.0: version "4.9.2" resolved "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz"