diff --git a/packages/api/src/beacon/routes/events.ts b/packages/api/src/beacon/routes/events.ts index 031b7fea513b..338771b19151 100644 --- a/packages/api/src/beacon/routes/events.ts +++ b/packages/api/src/beacon/routes/events.ts @@ -14,6 +14,7 @@ import { UintNum64, altair, capella, + eip7805, electra, phase0, ssz, @@ -76,6 +77,8 @@ export enum EventType { payloadAttributes = "payload_attributes", /** The node has received a valid blobSidecar (from P2P or API) */ blobSidecar = "blob_sidecar", + /** The node has received a valid inclusion list (from P2P or API) */ + inclusionList = "inclusion_list", } export const eventTypes: {[K in EventType]: K} = { @@ -94,6 +97,7 @@ export const eventTypes: {[K in EventType]: K} = { [EventType.lightClientFinalityUpdate]: EventType.lightClientFinalityUpdate, [EventType.payloadAttributes]: EventType.payloadAttributes, [EventType.blobSidecar]: EventType.blobSidecar, + [EventType.inclusionList]: EventType.inclusionList, }; export type EventData = { @@ -138,6 +142,7 @@ export type EventData = { [EventType.lightClientFinalityUpdate]: {version: ForkName; data: LightClientFinalityUpdate}; [EventType.payloadAttributes]: {version: ForkName; data: SSEPayloadAttributes}; [EventType.blobSidecar]: BlobSidecarSSE; + [EventType.inclusionList]: {version: ForkName; data: eip7805.SignedInclusionList}; }; export type BeaconEvent = {[K in EventType]: {type: K; message: EventData[K]}}[EventType]; @@ -291,6 +296,8 @@ export function getTypeByEvent(config: ChainForkConfig): {[K in EventType]: Type [EventType.lightClientFinalityUpdate]: WithVersion( (fork) => getLightClientForkTypes(fork).LightClientFinalityUpdate ), + + [EventType.inclusionList]: WithVersion(() => ssz.eip7805.SignedInclusionList), }; } diff --git a/packages/api/src/beacon/routes/validator.ts b/packages/api/src/beacon/routes/validator.ts index 313459f4db97..9f5e49b51473 100644 --- a/packages/api/src/beacon/routes/validator.ts +++ b/packages/api/src/beacon/routes/validator.ts @@ -14,6 +14,8 @@ import { UintBn64, ValidatorIndex, altair, + bellatrix, + eip7805, phase0, ssz, sszTypesFor, @@ -104,6 +106,20 @@ export const AttesterDutyType = new ContainerType( {jsonCase: "eth2"} ); +export const InclusionListDutyType = new ContainerType( + { + /** The validator's public key, uniquely identifying them */ + pubkey: ssz.BLSPubkey, + /** Index of validator in validator registry */ + validatorIndex: ssz.ValidatorIndex, + /** The slot at which the validator must propose the inclusion list*/ + slot: ssz.Slot, + /** Inclusion List Committee Root */ + inclusionListCommitteeRoot: ssz.Root, + }, + {jsonCase: "eth2"} +); + export const ProposerDutyType = new ContainerType( { slot: ssz.Slot, @@ -202,6 +218,7 @@ export const ValidatorIndicesType = ArrayOf(ssz.ValidatorIndex); export const AttesterDutyListType = ArrayOf(AttesterDutyType); export const ProposerDutyListType = ArrayOf(ProposerDutyType); export const SyncDutyListType = ArrayOf(SyncDutyType); +export const InclusionListDutyListType = ArrayOf(InclusionListDutyType); export const SignedAggregateAndProofListPhase0Type = ArrayOf(ssz.phase0.SignedAggregateAndProof); export const SignedAggregateAndProofListElectraType = ArrayOf(ssz.electra.SignedAggregateAndProof); export const SignedContributionAndProofListType = ArrayOf(ssz.altair.SignedContributionAndProof); @@ -220,6 +237,8 @@ export type ProposerDuty = ValueOf; export type ProposerDutyList = ValueOf; export type SyncDuty = ValueOf; export type SyncDutyList = ValueOf; +export type InclusionListDuty = ValueOf; +export type InclusionListDutyList = ValueOf; export type SignedAggregateAndProofListPhase0 = ValueOf; export type SignedAggregateAndProofListElectra = ValueOf; export type SignedAggregateAndProofList = SignedAggregateAndProofListPhase0 | SignedAggregateAndProofListElectra; @@ -288,6 +307,23 @@ export type Endpoints = { ExecutionOptimisticMeta >; + /** + * Get inclusion list committee duties + * Requests the beacon node to provide a set of inclusion list committee duties for a particular epoch. + */ + getInclusionListCommitteeDuties: Endpoint< + "POST", + { + /** Should only be allowed 1 epoch ahead */ + epoch: Epoch; + /** An array of the validator indices for which to obtain the duties */ + indices: ValidatorIndices; + }, + {params: {epoch: Epoch}; body: unknown}, + InclusionListDutyList, + ExecutionOptimisticAndDependentRootMeta + >; + /** * Requests a beacon node to produce a valid block, which can then be signed by a validator. * Metadata in the response indicates the type of block produced, and the supported types of block @@ -392,6 +428,21 @@ export type Endpoints = { EmptyMeta >; + /** + * Produce an inclusion list + * Requests the beacon node to produce an inclusion list. + */ + produceInclusionList: Endpoint< + "GET", + { + /** The slot for which an inclusion list should be created */ + slot: Slot; + }, + {query: {slot: number}}, + bellatrix.Transactions, + VersionMeta + >; + /** * Get aggregated attestation * Aggregates all attestations matching given attestation data root and slot @@ -459,6 +510,18 @@ export type Endpoints = { EmptyMeta >; + /** + * Publish inclusion list + * Verifies given inclusion list and publishes it on appropriate gossipsub topic. + */ + publishInclusionList: Endpoint< + "POST", + {signedInclusionList: eip7805.SignedInclusionList}, + {body: unknown; headers: {[MetaHeader.Version]: string}}, + EmptyResponseData, + EmptyMeta + >; + /** * Signal beacon node to prepare for a committee subnet * After beacon node receives this request, @@ -616,6 +679,24 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({params: {epoch}, body: ValidatorIndicesType.toJson(indices)}), + parseReqJson: ({params, body}) => ({epoch: params.epoch, indices: ValidatorIndicesType.fromJson(body)}), + writeReqSsz: ({epoch, indices}) => ({params: {epoch}, body: ValidatorIndicesType.serialize(indices)}), + parseReqSsz: ({params, body}) => ({epoch: params.epoch, indices: ValidatorIndicesType.deserialize(body)}), + schema: { + params: {epoch: Schema.UintRequired}, + body: Schema.StringArray, + }, + }, + resp: { + data: InclusionListDutyListType, + meta: ExecutionOptimisticAndDependentRootCodec, + }, + }, produceBlockV2: { url: "/eth/v2/validator/blocks/{slot}", method: "GET", @@ -825,6 +906,21 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions ({query: {slot}}), + parseReq: ({query}) => ({slot: query.slot}), + schema: { + query: {slot: Schema.UintRequired}, + }, + }, + resp: { + data: ssz.bellatrix.Transactions, + meta: VersionCodec, + }, + }, getAggregatedAttestation: { url: "/eth/v1/validator/aggregate_attestation", method: "GET", @@ -966,6 +1062,37 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { + const fork = config.getForkName(signedInclusionList.message.slot); + return { + body: ssz.eip7805.SignedInclusionList.toJson(signedInclusionList), + headers: { + [MetaHeader.Version]: fork, + }, + }; + }, + parseReqJson: ({body}) => ({signedInclusionList: ssz.eip7805.SignedInclusionList.fromJson(body)}), + writeReqSsz: ({signedInclusionList}) => { + const fork = config.getForkName(signedInclusionList.message.slot); + return { + body: ssz.eip7805.SignedInclusionList.serialize(signedInclusionList), + headers: { + [MetaHeader.Version]: fork, + }, + }; + }, + parseReqSsz: ({body}) => ({signedInclusionList: ssz.eip7805.SignedInclusionList.deserialize(body)}), + schema: { + body: Schema.Object, + headers: {[MetaHeader.Version]: Schema.String}, + }, + }, + resp: EmptyResponseCodec, + }, prepareBeaconCommitteeSubnet: { url: "/eth/v1/validator/beacon_committee_subscriptions", method: "POST", diff --git a/packages/api/src/utils/metadata.ts b/packages/api/src/utils/metadata.ts index 6186c06a7047..45d00af7b47f 100644 --- a/packages/api/src/utils/metadata.ts +++ b/packages/api/src/utils/metadata.ts @@ -114,7 +114,7 @@ export const ExecutionOptimisticAndVersionCodec: ResponseMetadataCodec ExecutionOptimisticAndVersionType.fromJson(val), toHeadersObject: (val) => ({ [MetaHeader.ExecutionOptimistic]: val.executionOptimistic.toString(), - [MetaHeader.Version]: val.version, + [MetaHeader.Version]: val.version === ForkName.eip7805 ? ForkName.electra : val.version, [HttpHeader.ExposeHeaders]: [MetaHeader.ExecutionOptimistic, MetaHeader.Version].toString(), }), fromHeaders: (headers) => ({ @@ -144,7 +144,7 @@ export const ExecutionOptimisticFinalizedAndVersionCodec: ResponseMetadataCodec< toHeadersObject: (val) => ({ [MetaHeader.ExecutionOptimistic]: val.executionOptimistic.toString(), [MetaHeader.Finalized]: val.finalized.toString(), - [MetaHeader.Version]: val.version, + [MetaHeader.Version]: val.version === ForkName.eip7805 ? ForkName.electra : val.version, [HttpHeader.ExposeHeaders]: [MetaHeader.ExecutionOptimistic, MetaHeader.Finalized, MetaHeader.Version].toString(), }), fromHeaders: (headers) => ({ diff --git a/packages/api/test/unit/beacon/testData/events.ts b/packages/api/test/unit/beacon/testData/events.ts index 81cf00d4010f..0f1a58ba9a62 100644 --- a/packages/api/test/unit/beacon/testData/events.ts +++ b/packages/api/test/unit/beacon/testData/events.ts @@ -254,4 +254,18 @@ export const eventTestData: EventData = { slot: "1", versioned_hash: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", }), + // TODO EIP-7805: update example value + [EventType.inclusionList]: { + version: ForkName.eip7805, + data: ssz.eip7805.SignedInclusionList.fromJson({ + message: { + slot: "0", + validator_index: "0", + inclusion_list_committee_root: "0x0000000000000000000000000000000000000000000000000000000000000000", + transactions: ["0x0000000000000000000000000000000000000000000000000000000000000000"], + }, + signature: + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + }), + }, }; diff --git a/packages/api/test/unit/beacon/testData/validator.ts b/packages/api/test/unit/beacon/testData/validator.ts index 327100cedab1..4374ea77a2a5 100644 --- a/packages/api/test/unit/beacon/testData/validator.ts +++ b/packages/api/test/unit/beacon/testData/validator.ts @@ -42,6 +42,20 @@ export const testData: GenericServerTestCases = { meta: {executionOptimistic: true}, }, }, + getInclusionListCommitteeDuties: { + args: {epoch: 0, indices: [1, 2, 3]}, + res: { + data: [ + { + pubkey: new Uint8Array(48).fill(1), + validatorIndex: 2, + slot: 3, + inclusionListCommitteeRoot: ZERO_HASH, + }, + ], + meta: {executionOptimistic: true, dependentRoot: ZERO_HASH_HEX}, + }, + }, produceBlockV2: { args: { slot: 32000, @@ -98,6 +112,10 @@ export const testData: GenericServerTestCases = { args: {slot: 32000, subcommitteeIndex: 2, beaconBlockRoot: ZERO_HASH}, res: {data: ssz.altair.SyncCommitteeContribution.defaultValue()}, }, + produceInclusionList: { + args: {slot: 32000}, + res: {data: [ssz.bellatrix.Transaction.defaultValue()], meta: {version: ForkName.eip7805}}, + }, getAggregatedAttestation: { args: {attestationDataRoot: ZERO_HASH, slot: 32000}, res: {data: ssz.phase0.Attestation.defaultValue()}, @@ -118,6 +136,10 @@ export const testData: GenericServerTestCases = { args: {contributionAndProofs: [ssz.altair.SignedContributionAndProof.defaultValue()]}, res: undefined, }, + publishInclusionList: { + args: {signedInclusionList: ssz.eip7805.SignedInclusionList.defaultValue()}, + res: undefined, + }, prepareBeaconCommitteeSubnet: { args: {subscriptions: [{validatorIndex: 1, committeeIndex: 2, committeesAtSlot: 3, slot: 4, isAggregator: true}]}, res: undefined, diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 66935df25b2b..ebb5950890b9 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -458,7 +458,7 @@ export function getBeaconBlockApi({ }; } } else if (blockId === "head") { - const head = chain.forkChoice.getHead(); + const head = chain.forkChoice.getAttesterHead(); return { data: {root: fromHex(head.blockRoot)}, meta: {executionOptimistic: isOptimisticBlock(head), finalized: false}, diff --git a/packages/beacon-node/src/api/impl/beacon/state/index.ts b/packages/beacon-node/src/api/impl/beacon/state/index.ts index 916507cc56fc..e06679c46731 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/index.ts @@ -239,7 +239,7 @@ export function getBeaconStateApi({ `No shuffling found to calculate committees for epoch: ${epoch} and decisionRoot: ${decisionRoot}` ); } - const committees = shuffling.committees; + const committees = shuffling.beaconCommittees; const committeesFlat = committees.flatMap((slotCommittees, slotInEpoch) => { const slot = startSlot + slotInEpoch; if (filters.slot !== undefined && filters.slot !== slot) { diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index fc8934ef5617..5265e2dae158 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -13,13 +13,15 @@ import { SYNC_COMMITTEE_SUBNET_SIZE, isForkBlobs, isForkExecution, + isForkPostEip7805, isForkPostElectra, } from "@lodestar/params"; import { CachedBeaconStateAllForks, attesterShufflingDecisionRoot, beaconBlockToBlinded, - calculateCommitteeAssignments, + calculateBeaconCommitteeAssignments, + calculateInclusionListCommitteeAssignments, computeEpochAtSlot, computeStartSlotAtEpoch, createCachedBeaconState, @@ -40,6 +42,7 @@ import { ValidatorIndex, Wei, bellatrix, + eip7805, getValidatorStatus, isBlindedBeaconBlock, isBlockContents, @@ -66,7 +69,7 @@ import { import {ChainEvent, CheckpointHex, CommonBlockBody} from "../../../chain/index.js"; import {SCHEDULER_LOOKAHEAD_FACTOR} from "../../../chain/prepareNextSlot.js"; import {RegenCaller} from "../../../chain/regen/index.js"; -import {validateApiAggregateAndProof} from "../../../chain/validation/index.js"; +import {validateApiAggregateAndProof, validateApiInclusionList} from "../../../chain/validation/index.js"; import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/validation/syncCommitteeContributionAndProof.js"; import {ZERO_HASH} from "../../../constants/index.js"; import {NoBidReceived} from "../../../execution/builder/http.js"; @@ -77,6 +80,7 @@ import {isOptimisticBlock} from "../../../util/forkChoice.js"; import {getDefaultGraffiti, toGraffitiBuffer} from "../../../util/graffiti.js"; import {getLodestarClientVersion} from "../../../util/metadata.js"; import {ApiOptions} from "../../options.js"; +import {getBlockResponse} from "../beacon/blocks/utils.js"; import {getStateResponseWithRegen} from "../beacon/state/utils.js"; import {ApiError, NodeIsSyncing, OnlySupportedByDVT} from "../errors.js"; import {ApiModules} from "../types.js"; @@ -916,10 +920,10 @@ export function getValidatorApi( // This needs a state in the same epoch as `slot` such that state.currentJustifiedCheckpoint is correct. // Note: This may trigger an epoch transition if there skipped slots at the beginning of the epoch. - const headState = chain.getHeadState(); + const headState = chain.getAttesterHeadState(); const headSlot = headState.slot; const attEpoch = computeEpochAtSlot(slot); - const headBlockRootHex = chain.forkChoice.getHead().blockRoot; + const headBlockRootHex = chain.forkChoice.getAttesterHead().blockRoot; const headBlockRoot = fromHex(headBlockRootHex); const fork = config.getForkName(slot); @@ -1002,6 +1006,43 @@ export function getValidatorApi( return {data: contribution}; }, + async produceInclusionList({slot}) { + notWhileSyncing(); + + const fork = chain.config.getForkName(slot); + if (!isForkPostEip7805(fork)) { + throw new ApiError(400, `Producing inclusion list for pre-eip7805 slot: ${slot}`); + } + + await waitForSlot(slot); // Must never request for a future slot > currentSlot + + // This needs a state in the same epoch as `slot` such that state.currentJustifiedCheckpoint is correct. + // Note: This may trigger an epoch transition if there skipped slots at the beginning of the epoch. + const headState = chain.getAttesterHeadState(); + const headSlot = headState.slot; + const headBlockRootHex = chain.forkChoice.getAttesterHead().blockRoot; + + const beaconBlockRootHex = + slot >= headSlot + ? // When attesting to the head slot or later, always use the head of the chain. + headBlockRootHex + : // Permit attesting to slots *prior* to the current head. This is desirable when + // the VC and BN are out-of-sync due to time issues or overloading. + toHex(getBlockRootAtSlot(headState, slot)); + + const block = (await getBlockResponse(chain, beaconBlockRootHex)).block; + const executionPayload = (block as eip7805.SignedBeaconBlock).message.body.executionPayload; + const blockHash = toHex(executionPayload.blockHash); + logger.debug("produce inclusion list", {blockHash}); + + const ilTransactions = await chain.executionEngine.getInclusionList(blockHash); + + return { + data: ilTransactions, + meta: {version: config.getForkName(slot)}, + }; + }, + async getProposerDuties({epoch}) { notWhileSyncing(); @@ -1153,7 +1194,7 @@ export function getValidatorApi( `No shuffling found to calculate committee assignments for epoch: ${epoch} and decisionRoot: ${decisionRoot}` ); } - const committeeAssignments = calculateCommitteeAssignments(shuffling, indices); + const committeeAssignments = calculateBeaconCommitteeAssignments(shuffling, indices); const duties: routes.validator.AttesterDuty[] = []; for (let i = 0, len = indices.length; i < len; i++) { const validatorIndex = indices[i]; @@ -1231,6 +1272,73 @@ export function getValidatorApi( }; }, + async getInclusionListCommitteeDuties({epoch, indices}) { + notWhileSyncing(); + + if (indices.length === 0) { + throw new ApiError(400, "No validator to get inclusion list committee duties"); + } + + const fork = chain.config.getForkName(computeStartSlotAtEpoch(epoch)); + if (!isForkPostEip7805(fork)) { + throw new ApiError(400, `Requesting pre-eip7805 inclusion list committee duties epoch: ${epoch}`); + } + + // May request for an epoch that's in the future + await waitForNextClosestEpoch(); + + // should not compare to headEpoch in order to handle skipped slots + // Check if the epoch is in the future after waiting for requested slot + if (epoch > chain.clock.currentEpoch + 1) { + throw new ApiError(400, "Cannot get duties for epoch more than one ahead"); + } + + const head = chain.forkChoice.getHead(); + const state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.getDuties); + + // TODO: Determine what the current epoch would be if we fast-forward our system clock by + // `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. + // + // Most of the time, `tolerantCurrentEpoch` will be equal to `currentEpoch`. However, during + // the first `MAXIMUM_GOSSIP_CLOCK_DISPARITY` duration of the epoch `tolerantCurrentEpoch` + // will equal `currentEpoch + 1` + + // Check that all validatorIndex belong to the state before calling getCommitteeAssignments() + const pubkeys = getPubkeysForIndices(state.validators, indices); + const decisionRoot = state.epochCtx.getShufflingDecisionRoot(epoch); + const shuffling = await chain.shufflingCache.get(epoch, decisionRoot); + if (!shuffling) { + throw new ApiError( + 500, + `No shuffling found to calculate committee assignments for epoch: ${epoch} and decisionRoot: ${decisionRoot}` + ); + } + const committeeAssignments = calculateInclusionListCommitteeAssignments(shuffling, indices); + const duties: routes.validator.InclusionListDuty[] = []; + for (let i = 0, len = indices.length; i < len; i++) { + const validatorIndex = indices[i]; + const duty = committeeAssignments.get(validatorIndex); + if (duty) { + duties.push({ + pubkey: pubkeys[i], + validatorIndex, + slot: duty.slot, + inclusionListCommitteeRoot: duty.committeeRoot, + }); + } + } + + const dependentRoot = attesterShufflingDecisionRoot(state, epoch) || (await getGenesisBlockRoot(state)); + + return { + data: duties, + meta: { + dependentRoot: toRootHex(dependentRoot), + executionOptimistic: isOptimisticBlock(head), + }, + }; + }, + async getAggregatedAttestation({attestationDataRoot, slot}) { notWhileSyncing(); @@ -1404,6 +1512,31 @@ export function getValidatorApi( } }, + async publishInclusionList({signedInclusionList}) { + notWhileSyncing(); + + // TODO EIP-7805: Add error handling + const slot = signedInclusionList.message.slot; + const fork = chain.config.getForkName(slot); + if (!isForkPostEip7805(fork)) { + throw new ApiError(400, `Publishing pre-eip7805 inclusion list slot: ${slot}`); + } + + await validateApiInclusionList(chain, signedInclusionList); + + chain.inclusionListPool.add(signedInclusionList); + + const secFromSlot = chain.clock.secFromSlot(slot, Date.now() / 1000); + chain.forkChoice.onInclusionList(signedInclusionList, secFromSlot); + + chain.emitter.emit(routes.events.EventType.inclusionList, { + version: config.getForkName(signedInclusionList.message.slot), + data: signedInclusionList, + }); + + await network.publishInclusionList(signedInclusionList); + }, + async prepareBeaconCommitteeSubnet({subscriptions}) { notWhileSyncing(); diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts index e37442bfde8f..303ef26bd8de 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts @@ -8,7 +8,7 @@ import { ProtoBlock, assertValidTerminalPowBlock, } from "@lodestar/fork-choice"; -import {ForkSeq, SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY} from "@lodestar/params"; +import {ForkSeq, SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY, isForkPostEip7805} from "@lodestar/params"; import { CachedBeaconStateAllForks, isExecutionBlockBodyType, @@ -26,6 +26,7 @@ import {Metrics} from "../../metrics/metrics.js"; import {kzgCommitmentToVersionedHash} from "../../util/blobs.js"; import {IClock} from "../../util/clock.js"; import {BlockError, BlockErrorCode} from "../errors/index.js"; +import {InclusionListPool} from "../opPools/inclusionListPool.js"; import {BlockProcessOpts} from "../options.js"; import {ImportBlockOpts} from "./types.js"; @@ -37,6 +38,7 @@ export type VerifyBlockExecutionPayloadModules = { metrics: Metrics | null; forkChoice: IForkChoice; config: ChainForkConfig; + inclusionListPool: InclusionListPool; }; type ExecAbortType = {blockIndex: number; execError: BlockError}; @@ -304,6 +306,7 @@ export async function verifyBlockExecutionPayload( const parentBlockRoot = ForkSeq[fork] >= ForkSeq.deneb ? block.message.parentRoot : undefined; const executionRequests = ForkSeq[fork] >= ForkSeq.electra ? (block.message.body as electra.BeaconBlockBody).executionRequests : undefined; + const ilTransactions = isForkPostEip7805(fork) ? chain.inclusionListPool.getTransactions(currentSlot) : undefined; const logCtx = {slot: block.message.slot, executionBlock: executionPayloadEnabled.blockNumber}; chain.logger.debug("Call engine api newPayload", logCtx); @@ -312,7 +315,8 @@ export async function verifyBlockExecutionPayload( executionPayloadEnabled, versionedHashes, parentBlockRoot, - executionRequests + executionRequests, + ilTransactions ); chain.logger.debug("Receive engine api newPayload result", {...logCtx, status: execResult.status}); @@ -377,6 +381,20 @@ export async function verifyBlockExecutionPayload( // back. But for now, lets assume other mechanisms like unknown parent block of a future // child block will cause it to replay + case ExecutionPayloadStatus.INVALID_INCLUSION_LIST: { + // Add IL-unsatisified block to fcstore + const blockRoot = toRootHex( + chain.config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message) + ); + chain.forkChoice.addInclusionListUnsatisfiedBlock(blockRoot); + + const execError = new BlockError(block, { + code: BlockErrorCode.EXECUTION_ENGINE_ERROR, + execStatus: execResult.status, + errorMessage: execResult.validationError, + }); + return {executionStatus: null, execError} as VerifyBlockExecutionResponse; + } case ExecutionPayloadStatus.INVALID_BLOCK_HASH: case ExecutionPayloadStatus.ELERROR: case ExecutionPayloadStatus.UNAVAILABLE: { diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 415158abb2d5..5f6312ac2293 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -69,6 +69,7 @@ import {LightClientServer} from "./lightClient/index.js"; import { AggregatedAttestationPool, AttestationPool, + InclusionListPool, OpPool, SyncCommitteeMessagePool, SyncContributionAndProofPool, @@ -137,6 +138,7 @@ export class BeaconChain implements IBeaconChain { readonly aggregatedAttestationPool: AggregatedAttestationPool; readonly syncCommitteeMessagePool: SyncCommitteeMessagePool; readonly syncContributionAndProofPool = new SyncContributionAndProofPool(); + readonly inclusionListPool: InclusionListPool; readonly opPool = new OpPool(); // Gossip seen cache @@ -239,6 +241,7 @@ export class BeaconChain implements IBeaconChain { preAggregateCutOffTime, this.opts?.preaggregateSlotDistance ); + this.inclusionListPool = new InclusionListPool(config, clock); this.seenAggregatedAttestations = new SeenAggregatedAttestations(metrics); this.seenContributionAndProof = new SeenContributionAndProof(metrics); @@ -419,6 +422,17 @@ export class BeaconChain implements IBeaconChain { return headState; } + // TODO EIP-7805: Temporary clone of `getHeadState()` to minimize impact on other parts that relies to real head + getAttesterHeadState(): CachedBeaconStateAllForks { + // head state should always exist + const head = this.forkChoice.getAttesterHead(); + const headState = this.regen.getClosestHeadState(head); + if (!headState) { + throw Error(`headState does not exist for head root=${head.blockRoot} slot=${head.slot}`); + } + return headState; + } + async getHeadStateAtCurrentEpoch(regenCaller: RegenCaller): Promise { return this.getHeadStateAtEpoch(this.clock.currentEpoch, regenCaller); } @@ -1037,6 +1051,7 @@ export class BeaconChain implements IBeaconChain { metrics.opPool.voluntaryExitPoolSize.set(this.opPool.voluntaryExitsSize); metrics.opPool.syncCommitteeMessagePoolSize.set(this.syncCommitteeMessagePool.size); metrics.opPool.syncContributionAndProofPoolSize.set(this.syncContributionAndProofPool.size); + metrics.opPool.inclusionListPoolSize.set(this.inclusionListPool.size); metrics.opPool.blsToExecutionChangePoolSize.set(this.opPool.blsToExecutionChangeSize); const forkChoiceMetrics = this.forkChoice.getMetrics(); @@ -1062,6 +1077,7 @@ export class BeaconChain implements IBeaconChain { this.attestationPool.prune(slot); this.aggregatedAttestationPool.prune(slot); this.syncCommitteeMessagePool.prune(slot); + this.inclusionListPool.prune(slot); this.seenSyncCommitteeMessages.prune(slot); this.seenAttestationDatas.onSlot(slot); this.reprocessController.onSlot(slot); diff --git a/packages/beacon-node/src/chain/errors/inclusionList.ts b/packages/beacon-node/src/chain/errors/inclusionList.ts new file mode 100644 index 000000000000..d9001cb34302 --- /dev/null +++ b/packages/beacon-node/src/chain/errors/inclusionList.ts @@ -0,0 +1,24 @@ +import {Root, Slot, ValidatorIndex} from "@lodestar/types"; +import {GossipActionError} from "./gossipValidation.js"; + +export enum InclusionListErrorCode { + MAXIMUM_SIZE_EXCEEDED = "INCLUSION_LIST_ERROR_MAXIMUM_SIZE_EXCEEDED", + INVALID_SLOT = "INCLUSION_LIST_ERROR_INVALID_SLOT", + NOT_TIMELY = "INCLUSION_LIST_ERROR_NOT_TIMELY", + INVALID_COMMITTEE_ROOT = "INCLUSION_LIST_ERROR_INVALID_COMMITTEE_ROOT", + VALIDATOR_NOT_IN_COMMITTEE = "INCLUSION_LIST_ERROR_VALIDATOR_NOT_IN_COMMITTEE", + SPAM = "INCLUSION_LIST_ERROR_SPAM", + INVALID_SIGNATURE = "INCLUSION_LIST_ERROR_INVALID_SIGNATURE", + MORE_THAN_TWO = "INCLUSION_LIST_ERROR_MORE_THAN_TWO", +} +export type InclusionListErrorType = + | {code: InclusionListErrorCode.MAXIMUM_SIZE_EXCEEDED; inclusionListSize: number; sizeLimit: number} + | {code: InclusionListErrorCode.INVALID_SLOT; inclusionListSlot: Slot; currentSlot: Slot} + | {code: InclusionListErrorCode.NOT_TIMELY} + | {code: InclusionListErrorCode.INVALID_COMMITTEE_ROOT; received: Root; expected: Root} + | {code: InclusionListErrorCode.VALIDATOR_NOT_IN_COMMITTEE; validatorIndex: number; committee: Uint32Array} + | {code: InclusionListErrorCode.SPAM} + | {code: InclusionListErrorCode.INVALID_SIGNATURE} + | {code: InclusionListErrorCode.MORE_THAN_TWO; validatorIndex: ValidatorIndex}; + +export class InclusionListError extends GossipActionError {} diff --git a/packages/beacon-node/src/chain/forkChoice/index.ts b/packages/beacon-node/src/chain/forkChoice/index.ts index b3f5d11260f2..a8faa55561c1 100644 --- a/packages/beacon-node/src/chain/forkChoice/index.ts +++ b/packages/beacon-node/src/chain/forkChoice/index.ts @@ -17,6 +17,7 @@ import { } from "@lodestar/state-transition"; import {Slot} from "@lodestar/types"; +import {isForkPostEip7805} from "@lodestar/params"; import {Logger, toRootHex} from "@lodestar/utils"; import {GENESIS_SLOT} from "../../constants/index.js"; import {ChainEventEmitter} from "../emitter.js"; @@ -61,6 +62,8 @@ export function initializeForkChoice( // production code use ForkChoice constructor directly const forkchoiceConstructor = opts.forkchoiceConstructor ?? ForkChoice; + const isEip7805Enabled = isForkPostEip7805(config.getForkName(currentSlot)); + return new forkchoiceConstructor( config, @@ -83,6 +86,7 @@ export function initializeForkChoice( stateRoot: toRootHex(blockHeader.stateRoot), blockRoot: toRootHex(checkpoint.root), timeliness: true, // Optimisitcally assume is timely + isEip7805Enabled, justifiedEpoch: justifiedCheckpoint.epoch, justifiedRoot: toRootHex(justifiedCheckpoint.root), diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 67b011ec0246..84efe227b6b0 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -39,7 +39,13 @@ import {ChainEventEmitter} from "./emitter.js"; import {ForkchoiceCaller} from "./forkChoice/index.js"; import {LightClientServer} from "./lightClient/index.js"; import {AggregatedAttestationPool} from "./opPools/aggregatedAttestationPool.js"; -import {AttestationPool, OpPool, SyncCommitteeMessagePool, SyncContributionAndProofPool} from "./opPools/index.js"; +import { + AttestationPool, + InclusionListPool, + OpPool, + SyncCommitteeMessagePool, + SyncContributionAndProofPool, +} from "./opPools/index.js"; import {IChainOptions} from "./options.js"; import {AssembledBlockType, BlockAttributes, BlockType} from "./produceBlock/produceBlockBody.js"; import {IStateRegenerator, RegenCaller} from "./regen/index.js"; @@ -108,6 +114,7 @@ export interface IBeaconChain { readonly aggregatedAttestationPool: AggregatedAttestationPool; readonly syncCommitteeMessagePool: SyncCommitteeMessagePool; readonly syncContributionAndProofPool: SyncContributionAndProofPool; + readonly inclusionListPool: InclusionListPool; readonly opPool: OpPool; // Gossip seen cache @@ -142,6 +149,7 @@ export interface IBeaconChain { validatorSeenAtEpoch(index: ValidatorIndex, epoch: Epoch): boolean; getHeadState(): CachedBeaconStateAllForks; + getAttesterHeadState(): CachedBeaconStateAllForks; getHeadStateAtCurrentEpoch(regenCaller: RegenCaller): Promise; getHeadStateAtEpoch(epoch: Epoch, regenCaller: RegenCaller): Promise; diff --git a/packages/beacon-node/src/chain/opPools/inclusionListPool.ts b/packages/beacon-node/src/chain/opPools/inclusionListPool.ts new file mode 100644 index 000000000000..0ae279f82aa7 --- /dev/null +++ b/packages/beacon-node/src/chain/opPools/inclusionListPool.ts @@ -0,0 +1,134 @@ +import {ChainForkConfig} from "@lodestar/config"; +import {INCLUSION_LIST_COMMITTEE_SIZE} from "@lodestar/params"; +import {Slot, ValidatorIndex, bellatrix, eip7805} from "@lodestar/types"; +import {MapDef} from "@lodestar/utils"; +import {byteArrayEquals} from "../../util/bytes.js"; +import {IClock} from "../../util/clock.js"; +import {OpPoolError, OpPoolErrorCode} from "./types.js"; +import {pruneBySlot} from "./utils.js"; + +/** + * + */ +const SLOTS_RETAINED = 2; // TODO EIP-7805: do we even need to retain previous slot? + +/** + * The maximum number of distinct `SignedInclusionList` that will be stored in each slot. + * + * This is a DoS protection measure. + */ +const MAX_INCLUSION_LISTS_PER_SLOT = INCLUSION_LIST_COMMITTEE_SIZE * 2; + +type CachedInclusionList = { + transactions: bellatrix.Transactions; + seenTwice: boolean; +}; + +export enum InclusionListInsertOutcome { + /** */ + New = "New", + /** Not existing in the pool but it's too old to add. No changes were made. */ + Old = "Old", + /** The pool has reached its limit. No changes were made. */ + ReachLimit = "ReachLimit", + /** */ + Late = "Late", + /** */ + SeenTwice = "SeenTwice", +} + +/** + * + */ +export class InclusionListPool { + private readonly inclusionListByValidatorBySlot = new MapDef>( + () => new Map() + ); + + private lowestPermissibleSlot = 0; + + constructor( + private readonly config: ChainForkConfig, + private readonly clock: IClock + ) {} + + get size(): number { + let count = 0; + for (const inclusionListsByValidator of this.inclusionListByValidatorBySlot.values()) { + count += Array.from(inclusionListsByValidator.values()).length; + } + return count; + } + + add(inclusionList: eip7805.SignedInclusionList): InclusionListInsertOutcome { + const {slot, validatorIndex, transactions} = inclusionList.message; + + // Reject any inclusion lists that are too old. + if (slot < this.lowestPermissibleSlot) { + return InclusionListInsertOutcome.Old; + } + + // Reject inclusion lists in the current slot but come to this pool very late + // TODO EIP-7805: review if this is correct + if (this.clock.secFromSlot(slot) > this.config.PROPOSER_INCLUSION_LIST_CUT_OFF) { + return InclusionListInsertOutcome.Late; + } + + // Limit object per slot + const inclusionListsByValidator = this.inclusionListByValidatorBySlot.getOrDefault(slot); + if (inclusionListsByValidator.size >= MAX_INCLUSION_LISTS_PER_SLOT) { + throw new OpPoolError({code: OpPoolErrorCode.REACHED_MAX_PER_SLOT}); + } + + // Track equivocations + const inclusionListByValidator = inclusionListsByValidator.get(validatorIndex); + if (inclusionListByValidator) { + inclusionListByValidator.seenTwice = true; + return InclusionListInsertOutcome.SeenTwice; + } + + // Create new inclusion list + inclusionListsByValidator.set(validatorIndex, {transactions, seenTwice: false}); + return InclusionListInsertOutcome.New; + } + + /** + * Return a list of unique inclusion list transactions for the given slot + */ + getTransactions(slot: Slot): bellatrix.Transactions { + const uniqueTransactions: bellatrix.Transactions = []; + + const inclusionListsByValidator = this.inclusionListByValidatorBySlot.get(slot); + if (!inclusionListsByValidator) { + return uniqueTransactions; + } + + for (const {transactions, seenTwice} of inclusionListsByValidator.values()) { + if (seenTwice) { + continue; + } + + for (const transaction of transactions) { + const duplicate = uniqueTransactions.some((existing) => byteArrayEquals(transaction, existing)); + + if (!duplicate) { + uniqueTransactions.push(transaction); + } + } + } + + return uniqueTransactions; + } + + seenTwice(slot: Slot, validatorIndex: ValidatorIndex): boolean { + return this.inclusionListByValidatorBySlot.get(slot)?.get(validatorIndex)?.seenTwice === true; + } + + /** + * + */ + prune(clockSlot: Slot): void { + pruneBySlot(this.inclusionListByValidatorBySlot, clockSlot, SLOTS_RETAINED); + this.lowestPermissibleSlot = Math.max(clockSlot - SLOTS_RETAINED, 0); + } +} diff --git a/packages/beacon-node/src/chain/opPools/index.ts b/packages/beacon-node/src/chain/opPools/index.ts index 03cd9f395c6d..fffbdd76f984 100644 --- a/packages/beacon-node/src/chain/opPools/index.ts +++ b/packages/beacon-node/src/chain/opPools/index.ts @@ -2,4 +2,5 @@ export {AggregatedAttestationPool} from "./aggregatedAttestationPool.js"; export {AttestationPool} from "./attestationPool.js"; export {SyncCommitteeMessagePool} from "./syncCommitteeMessagePool.js"; export {SyncContributionAndProofPool} from "./syncContributionAndProofPool.js"; +export {InclusionListPool, InclusionListInsertOutcome} from "./inclusionListPool.js"; export {OpPool} from "./opPool.js"; diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index f78c1842bd78..ba723d68770d 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -1,6 +1,6 @@ import {routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; -import {ForkExecution, ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params"; +import {ForkExecution, ForkSeq, SLOTS_PER_EPOCH, isForkPostEip7805} from "@lodestar/params"; import { CachedBeaconStateAllForks, CachedBeaconStateExecutions, @@ -12,12 +12,17 @@ import { import {Slot} from "@lodestar/types"; import {Logger, fromHex, isErrorAborted, sleep} from "@lodestar/utils"; import {GENESIS_SLOT, ZERO_HASH_HEX} from "../constants/constants.js"; +import {PayloadId} from "../execution/index.js"; import {Metrics} from "../metrics/index.js"; import {ClockEvent} from "../util/clock.js"; import {isQueueErrorAborted} from "../util/queue/index.js"; import {ForkchoiceCaller} from "./forkChoice/index.js"; import {IBeaconChain} from "./interface.js"; -import {getPayloadAttributesForSSE, prepareExecutionPayload} from "./produceBlock/produceBlockBody.js"; +import { + getPayloadAttributesForSSE, + prepareExecutionPayload, + prepareExecutionPayloadInclusionList, +} from "./produceBlock/produceBlockBody.js"; import {RegenCaller} from "./regen/index.js"; /* With 12s slot times, this scheduler will run 4s before the start of each slot (`12 / 3 = 4`). */ @@ -169,7 +174,7 @@ export class PrepareNextSlotScheduler { // awaiting here instead of throwing an async call because there is no other task // left for scheduler and this gives nice sematics to catch and log errors in the // try/catch wrapper here. - await prepareExecutionPayload( + const {payloadId} = (await prepareExecutionPayload( this.chain, this.logger, fork as ForkExecution, // State is of execution type @@ -178,18 +183,25 @@ export class PrepareNextSlotScheduler { finalizedBlockHash, updatedPrepareState, feeRecipient - ); + )) as {payloadId: PayloadId}; this.logger.verbose("PrepareNextSlotScheduler prepared new payload", { prepareSlot, proposerIndex, feeRecipient, }); + + if (isForkPostEip7805(fork)) { + this.schedulePayloadInclusionListUpdate(payloadId, clockSlot).catch((e) => { + this.logger.error("Failed to update payload with inclusion list", {payloadId, prepareSlot}, e); + }); + } } this.computeStateHashTreeRoot(updatedPrepareState, isEpochTransition); // If emitPayloadAttributes is true emit a SSE payloadAttributes event if (this.chain.opts.emitPayloadAttributes === true) { + // TODO EIP-7805: do we wanna emit data about inclusion lists here const data = await getPayloadAttributesForSSE(fork as ForkExecution, this.chain, { prepareState: updatedPrepareState, prepareSlot, @@ -240,4 +252,16 @@ export class PrepareNextSlotScheduler { state.hashTreeRoot(); hashTreeRootTimer?.(); } + + /** + * Schedule task to update payload with inclusion list transactions that gathered up to `PROPOSER_INCLUSION_LIST_CUT_OFF` + */ + async schedulePayloadInclusionListUpdate(payloadId: PayloadId, clockSlot: Slot): Promise { + // TODO EIP-7805: Hard-coding sleepTime to 500ms for testing purpose. Revert it back after done testing + // const sleepTime = Math.max(0.5, (this.config.PROPOSER_INCLUSION_LIST_CUT_OFF - this.chain.clock.secFromSlot(clockSlot)) * 1000); + const sleepTime = 0.5 * 1000; + await sleep(sleepTime, this.signal); + + await prepareExecutionPayloadInclusionList(this.chain, this.logger, payloadId, clockSlot); + } } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index cbd46b1655aa..f23551c26c18 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -46,6 +46,7 @@ import { import {fromGraffitiBuffer} from "../../util/graffiti.js"; import type {BeaconChain} from "../chain.js"; import {CommonBlockBody} from "../interface.js"; +import {InclusionListPool} from "../opPools/inclusionListPool.js"; import {validateBlobsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js"; // Time to provide the EL to generate a payload from new payload id @@ -503,6 +504,23 @@ export async function prepareExecutionPayload( return {isPremerge: false, payloadId, prepType}; } +export async function prepareExecutionPayloadInclusionList( + chain: {executionEngine: IExecutionEngine; inclusionListPool: InclusionListPool}, + logger: Logger, + payloadId: PayloadId, + slot: Slot +): Promise { + const transactions = chain.inclusionListPool.getTransactions(slot); + + await chain.executionEngine.updatePayloadWithInclusionList(payloadId, {transactions}); + + logger.verbose("Updated payload with inclusion list", { + slot, + payloadId, + numTransactions: transactions.length, + }); +} + async function prepareExecutionPayloadHeader( chain: { eth1: IEth1ForBlockProduction; diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index 95471534ef53..7d5f5976efcd 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -788,8 +788,8 @@ export function getCommitteeIndices( attestationSlot: Slot, attestationIndex: number ): Uint32Array { - const {committees} = shuffling; - const slotCommittees = committees[attestationSlot % SLOTS_PER_EPOCH]; + const {beaconCommittees} = shuffling; + const slotCommittees = beaconCommittees[attestationSlot % SLOTS_PER_EPOCH]; if (attestationIndex >= slotCommittees.length) { throw new AttestationError(GossipAction.REJECT, { @@ -805,7 +805,7 @@ export function getCommitteeIndices( */ export function computeSubnetForSlot(shuffling: EpochShuffling, slot: number, committeeIndex: number): SubnetID { const slotsSinceEpochStart = slot % SLOTS_PER_EPOCH; - const committeesSinceEpochStart = shuffling.committeesPerSlot * slotsSinceEpochStart; + const committeesSinceEpochStart = shuffling.beaconCommitteesPerSlot * slotsSinceEpochStart; return (committeesSinceEpochStart + committeeIndex) % ATTESTATION_SUBNET_COUNT; } diff --git a/packages/beacon-node/src/chain/validation/inclusionList.ts b/packages/beacon-node/src/chain/validation/inclusionList.ts new file mode 100644 index 000000000000..c0fa10a0d82b --- /dev/null +++ b/packages/beacon-node/src/chain/validation/inclusionList.ts @@ -0,0 +1,94 @@ +import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {computeEpochAtSlot, getInclusionListSignatureSet} from "@lodestar/state-transition"; +import {eip7805} from "@lodestar/types"; +import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; +import {InclusionListError, InclusionListErrorCode} from "../errors/inclusionList.js"; +import {GossipAction} from "../errors/index.js"; +import {IBeaconChain} from "../index.js"; + +export async function validateApiInclusionList( + chain: IBeaconChain, + inclusionList: eip7805.SignedInclusionList +): Promise { + return validateInclusionList(chain, inclusionList); +} + +export async function validateGossipInclusionList( + chain: IBeaconChain, + inclusionList: eip7805.SignedInclusionList +): Promise { + return validateInclusionList(chain, inclusionList); +} + +async function validateInclusionList(chain: IBeaconChain, inclusionList: eip7805.SignedInclusionList): Promise { + const {slot, validatorIndex, transactions, inclusionListCommitteeRoot} = inclusionList.message; + + // [REJECT] The size of message is within upperbound MAX_BYTES_PER_INCLUSION_LIST + // TODO EIP-7805: spec is outdated, we need to check total size of all transactions + const inclusionListSize = transactions.reduce((total, transaction) => total + transaction.byteLength, 0); + if (inclusionListSize > chain.config.MAX_BYTES_PER_INCLUSION_LIST) { + throw new InclusionListError(GossipAction.REJECT, { + code: InclusionListErrorCode.MAXIMUM_SIZE_EXCEEDED, + inclusionListSize, + sizeLimit: chain.config.MAX_BYTES_PER_INCLUSION_LIST, + }); + } + + // [REJECT] The slot message.slot is equal to the previous or current slot. + if (slot !== chain.clock.currentSlot && slot !== chain.clock.currentSlot - 1) { + throw new InclusionListError(GossipAction.REJECT, { + code: InclusionListErrorCode.INVALID_SLOT, + inclusionListSlot: slot, + currentSlot: chain.clock.currentSlot, + }); + } + + // [IGNORE] The slot message.slot is equal to the current slot, or it is equal to the previous slot and the current time is less than attestation_deadline seconds into the slot. + + const headBlock = chain.forkChoice.getHead(); // Head block in current branch + const headBlockEpoch = computeEpochAtSlot(headBlock.slot); + const ilEpoch = computeEpochAtSlot(slot); + const shufflingDependentRoot = getShufflingDependentRoot(chain.forkChoice, ilEpoch, headBlockEpoch, headBlock); + const shuffling = await chain.shufflingCache.get(ilEpoch, shufflingDependentRoot); + + if (shuffling === null) { + throw new Error("Shuffling not available"); // TODO EIP-7805: Handle shuffling cache miss + } + + // [IGNORE] The inclusion_list_committee for slot message.slot on the current branch corresponds to message.inclusion_list_committee_root, as determined by hash_tree_root(inclusion_list_committee) == message.inclusion_list_committee_root. + const inclusionListCommitteeRootFromShuffling = shuffling.inclusionListCommitteeRoots[slot % SLOTS_PER_EPOCH]; + if (Buffer.compare(inclusionListCommitteeRoot, inclusionListCommitteeRootFromShuffling) !== 0) { + throw new InclusionListError(GossipAction.IGNORE, { + code: InclusionListErrorCode.INVALID_COMMITTEE_ROOT, + received: inclusionListCommitteeRoot, + expected: inclusionListCommitteeRootFromShuffling, + }); + } + + // [REJECT] The validator index message.validator_index is within the inclusion_list_committee corresponding to message.inclusion_list_committee_root. + const inclusionListCommitteeFromShuffling = shuffling.inclusionListCommittees[slot % SLOTS_PER_EPOCH]; + if (!inclusionListCommitteeFromShuffling.includes(validatorIndex)) { + throw new InclusionListError(GossipAction.REJECT, { + code: InclusionListErrorCode.VALIDATOR_NOT_IN_COMMITTEE, + validatorIndex, + committee: inclusionListCommitteeFromShuffling, + }); + } + + // TODO EIP-7805: use a different cache similar to `seenAttesters` here? + // [IGNORE] The message is either the first or second valid message received from the validator with index message.validator_index. + if (chain.inclusionListPool.seenTwice(slot, validatorIndex)) { + throw new InclusionListError(GossipAction.IGNORE, { + code: InclusionListErrorCode.MORE_THAN_TWO, + validatorIndex, + }); + } + + // [REJECT] The signature of inclusion_list.signature is valid with respect to the validator index. + const signatureSet = getInclusionListSignatureSet(chain.getHeadState(), inclusionList); + if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true}))) { + throw new InclusionListError(GossipAction.REJECT, { + code: InclusionListErrorCode.INVALID_SIGNATURE, + }); + } +} diff --git a/packages/beacon-node/src/chain/validation/index.ts b/packages/beacon-node/src/chain/validation/index.ts index 468b79325bad..4ef8464db104 100644 --- a/packages/beacon-node/src/chain/validation/index.ts +++ b/packages/beacon-node/src/chain/validation/index.ts @@ -7,3 +7,4 @@ export * from "./syncCommittee.js"; export * from "./syncCommitteeContributionAndProof.js"; export * from "./voluntaryExit.js"; export * from "./blsToExecutionChange.js"; +export * from "./inclusionList.js"; diff --git a/packages/beacon-node/src/execution/engine/disabled.ts b/packages/beacon-node/src/execution/engine/disabled.ts index dce9244ef7ab..9e6ae3db36e0 100644 --- a/packages/beacon-node/src/execution/engine/disabled.ts +++ b/packages/beacon-node/src/execution/engine/disabled.ts @@ -32,4 +32,12 @@ export class ExecutionEngineDisabled implements IExecutionEngine { getBlobs(): Promise { throw Error("Execution engine disabled"); } + + updatePayloadWithInclusionList(): Promise { + throw Error("Execution engine disabled"); + } + + getInclusionList(): Promise { + throw Error("Execution engine disabled"); + } } diff --git a/packages/beacon-node/src/execution/engine/http.ts b/packages/beacon-node/src/execution/engine/http.ts index 0d2656ae46e2..d2d811bb6813 100644 --- a/packages/beacon-node/src/execution/engine/http.ts +++ b/packages/beacon-node/src/execution/engine/http.ts @@ -1,6 +1,6 @@ import {Logger} from "@lodestar/logger"; import {ForkName, ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params"; -import {ExecutionPayload, ExecutionRequests, Root, RootHex, Wei} from "@lodestar/types"; +import {ExecutionPayload, ExecutionRequests, Root, RootHex, Wei, bellatrix} from "@lodestar/types"; import {BlobAndProof} from "@lodestar/types/deneb"; import {strip0xPrefix} from "@lodestar/utils"; import { @@ -24,6 +24,7 @@ import { ExecutionEngineState, ExecutionPayloadStatus, IExecutionEngine, + InclusionList, PayloadAttributes, PayloadId, VersionedHashes, @@ -36,10 +37,12 @@ import { assertReqSizeLimit, deserializeBlobAndProofs, deserializeExecutionPayloadBody, + deserializeInclusionList, parseExecutionPayload, serializeBeaconBlockRoot, serializeExecutionPayload, serializeExecutionRequests, + serializeInclusionList, serializePayloadAttributes, serializeVersionedHashes, } from "./types.js"; @@ -202,19 +205,23 @@ export class ExecutionEngineHttp implements IExecutionEngine { executionPayload: ExecutionPayload, versionedHashes?: VersionedHashes, parentBlockRoot?: Root, - executionRequests?: ExecutionRequests + executionRequests?: ExecutionRequests, + inclusionListTransactions?: bellatrix.Transactions ): Promise { const method = - ForkSeq[fork] >= ForkSeq.electra - ? "engine_newPayloadV4" - : ForkSeq[fork] >= ForkSeq.deneb - ? "engine_newPayloadV3" - : ForkSeq[fork] >= ForkSeq.capella - ? "engine_newPayloadV2" - : "engine_newPayloadV1"; + ForkSeq[fork] >= ForkSeq.eip7805 + ? "engine_newPayloadV5" + : ForkSeq[fork] >= ForkSeq.electra + ? "engine_newPayloadV4" + : ForkSeq[fork] >= ForkSeq.deneb + ? "engine_newPayloadV3" + : ForkSeq[fork] >= ForkSeq.capella + ? "engine_newPayloadV2" + : "engine_newPayloadV1"; const serializedExecutionPayload = serializeExecutionPayload(fork, executionPayload); + // TODO EIP-7805: Add V5. Current code is ugly with all the nested if let engineRequest: EngineRequest; if (ForkSeq[fork] >= ForkSeq.deneb) { if (versionedHashes === undefined) { @@ -242,6 +249,23 @@ export class ExecutionEngineHttp implements IExecutionEngine { ], methodOpts: notifyNewPayloadOpts, }; + if (ForkSeq[fork] >= ForkSeq.eip7805) { + if (inclusionListTransactions === undefined) { + throw Error(`inclusionListTransactions required in notifyNewPayload for fork=${fork}`); + } + const serializedILTransactions = serializeInclusionList(inclusionListTransactions); + engineRequest = { + method: "engine_newPayloadV5", + params: [ + serializedExecutionPayload, + serializedVersionedHashes, + parentBeaconBlockRoot, + serializedExecutionRequests, + serializedILTransactions, + ], + methodOpts: notifyNewPayloadOpts, + }; + } } else { engineRequest = { method: "engine_newPayloadV3", @@ -517,6 +541,31 @@ export class ExecutionEngineHttp implements IExecutionEngine { return response.map(deserializeBlobAndProofs); } + async getInclusionList(parentHash: RootHex): Promise { + const method = "engine_getInclusionListV1"; + const response = await this.rpc.fetchWithRetries< + EngineApiRpcReturnTypes[typeof method], + EngineApiRpcParamTypes[typeof method] + >({ + method, + params: [parentHash], + }); + + return deserializeInclusionList(response); + } + + async updatePayloadWithInclusionList(payloadId: PayloadId, inclusionList: InclusionList): Promise { + const method = "engine_updatePayloadWithInclusionListV1"; + const result = await this.rpc.fetchWithRetries< + EngineApiRpcReturnTypes[typeof method], + EngineApiRpcParamTypes[typeof method] + >({ + method, + params: [payloadId, inclusionList.transactions.map(bytesToData)], + }); + return result !== "0x" ? payloadId : null; + } + private async getClientVersion(clientVersion: ClientVersion): Promise { const method = "engine_getClientVersionV1"; diff --git a/packages/beacon-node/src/execution/engine/interface.ts b/packages/beacon-node/src/execution/engine/interface.ts index 40ca06c4d2c2..6ef0ec7a4406 100644 --- a/packages/beacon-node/src/execution/engine/interface.ts +++ b/packages/beacon-node/src/execution/engine/interface.ts @@ -1,5 +1,5 @@ import {CONSOLIDATION_REQUEST_TYPE, DEPOSIT_REQUEST_TYPE, ForkName, WITHDRAWAL_REQUEST_TYPE} from "@lodestar/params"; -import {ExecutionPayload, ExecutionRequests, Root, RootHex, Wei, capella} from "@lodestar/types"; +import {ExecutionPayload, ExecutionRequests, Root, RootHex, Wei, bellatrix, capella} from "@lodestar/types"; import {Blob, BlobAndProof, KZGCommitment, KZGProof} from "@lodestar/types/deneb"; import {DATA} from "../../eth1/provider/utils.js"; @@ -28,6 +28,8 @@ export enum ExecutionPayloadStatus { UNAVAILABLE = "UNAVAILABLE", /** EL replied with SYNCING or ACCEPTED when its not safe to import optimistic blocks */ UNSAFE_OPTIMISTIC_STATUS = "UNSAFE_OPTIMISTIC_STATUS", + /** Payload does not satisfy the transactions in the provided inclusion lists */ + INVALID_INCLUSION_LIST = "INVALID_INCLUSION_LIST", } export enum ExecutionEngineState { @@ -79,6 +81,7 @@ export type ExecutePayloadResponse = status: | ExecutionPayloadStatus.INVALID_BLOCK_HASH | ExecutionPayloadStatus.ELERROR + | ExecutionPayloadStatus.INVALID_INCLUSION_LIST | ExecutionPayloadStatus.UNAVAILABLE; latestValidHash: null; validationError: string; @@ -118,6 +121,10 @@ export type ClientVersion = { export type VersionedHashes = Uint8Array[]; +export type InclusionList = { + transactions: Uint8Array[]; +}; + /** * Execution engine represents an abstract protocol to interact with execution clients. Potential transports include: * - JSON RPC over network @@ -144,7 +151,8 @@ export interface IExecutionEngine { executionPayload: ExecutionPayload, versionedHashes?: VersionedHashes, parentBeaconBlockRoot?: Root, - executionRequests?: ExecutionRequests + executionRequests?: ExecutionRequests, + inclusionListTransactions?: bellatrix.Transactions ): Promise; /** @@ -190,4 +198,8 @@ export interface IExecutionEngine { getPayloadBodiesByRange(fork: ForkName, start: number, count: number): Promise<(ExecutionPayloadBody | null)[]>; getBlobs(fork: ForkName, versionedHashes: VersionedHashes): Promise<(BlobAndProof | null)[]>; + + getInclusionList(parentHash: RootHex): Promise; + + updatePayloadWithInclusionList(payloadId: PayloadId, inclusionList: InclusionList): Promise; } diff --git a/packages/beacon-node/src/execution/engine/mock.ts b/packages/beacon-node/src/execution/engine/mock.ts index 8a84d2b0148e..eb20a161e591 100644 --- a/packages/beacon-node/src/execution/engine/mock.ts +++ b/packages/beacon-node/src/execution/engine/mock.ts @@ -89,6 +89,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { engine_newPayloadV2: this.notifyNewPayload.bind(this), engine_newPayloadV3: this.notifyNewPayload.bind(this), engine_newPayloadV4: this.notifyNewPayload.bind(this), + engine_newPayloadV5: this.notifyNewPayload.bind(this), engine_forkchoiceUpdatedV1: this.notifyForkchoiceUpdate.bind(this), engine_forkchoiceUpdatedV2: this.notifyForkchoiceUpdate.bind(this), engine_forkchoiceUpdatedV3: this.notifyForkchoiceUpdate.bind(this), @@ -100,6 +101,8 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { engine_getPayloadBodiesByRangeV1: this.getPayloadBodiesByRange.bind(this), engine_getClientVersionV1: this.getClientVersionV1.bind(this), engine_getBlobsV1: this.getBlobs.bind(this), + engine_getInclusionListV1: () => [], + engine_updatePayloadWithInclusionListV1: () => "0x", }; } diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index 24de1b9e6576..5762496c7e47 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -38,6 +38,7 @@ export type EngineApiRpcParamTypes = { engine_newPayloadV2: [ExecutionPayloadRpc]; engine_newPayloadV3: [ExecutionPayloadRpc, VersionedHashesRpc, DATA]; engine_newPayloadV4: [ExecutionPayloadRpc, VersionedHashesRpc, DATA, ExecutionRequestsRpc]; + engine_newPayloadV5: [ExecutionPayloadRpc, VersionedHashesRpc, DATA, ExecutionRequestsRpc, InclusionListRpc]; /** * 1. Object - Payload validity status with respect to the consensus rules: * - blockHash: DATA, 32 Bytes - block hash value of the payload @@ -80,6 +81,17 @@ export type EngineApiRpcParamTypes = { engine_getClientVersionV1: [ClientVersionRpc]; engine_getBlobsV1: [DATA[]]; + + /** + * 1. DATA - 32 bytes - parent hash which returned inclusion list should be built upon + */ + engine_getInclusionListV1: [DATA]; + + /** + * 1. DATA - 8 bytes - Identifier of the payload build process + * 2. DATA[] aka InclusionListV1 + */ + engine_updatePayloadWithInclusionListV1: [QUANTITY, InclusionListRpc]; }; export type PayloadStatus = { @@ -97,6 +109,7 @@ export type EngineApiRpcReturnTypes = { engine_newPayloadV2: PayloadStatus; engine_newPayloadV3: PayloadStatus; engine_newPayloadV4: PayloadStatus; + engine_newPayloadV5: PayloadStatus; engine_forkchoiceUpdatedV1: { payloadStatus: PayloadStatus; payloadId: QUANTITY | null; @@ -124,6 +137,10 @@ export type EngineApiRpcReturnTypes = { engine_getClientVersionV1: ClientVersionRpc[]; engine_getBlobsV1: (BlobAndProofRpc | null)[]; + + engine_getInclusionListV1: InclusionListRpc; + + engine_updatePayloadWithInclusionListV1: QUANTITY | null; }; type ExecutionPayloadRpcWithValue = { @@ -222,6 +239,9 @@ export interface BlobsBundleRpc { proofs: DATA[]; // some ELs could also provide proofs, each 48 bytes } +/** Array of DATA - Array of transaction objects */ +type InclusionListRpc = DATA[]; + export function serializeExecutionPayload(fork: ForkName, data: ExecutionPayload): ExecutionPayloadRpc { const payload: ExecutionPayloadRpc = { parentHash: bytesToData(data.parentHash), @@ -557,6 +577,14 @@ export function deserializeBlobAndProofs(data: BlobAndProofRpc | null): BlobAndP : null; } +export function serializeInclusionList(data: bellatrix.Transactions): InclusionListRpc { + return data.map((tran) => bytesToData(tran)); +} + +export function deserializeInclusionList(data: InclusionListRpc): bellatrix.Transactions { + return data.map((tran) => dataToBytes(tran, null)); +} + export function assertReqSizeLimit(blockHashesReqCount: number, count: number): void { if (blockHashesReqCount > count) { throw new Error(`Requested blocks must not be > ${count}`); diff --git a/packages/beacon-node/src/execution/engine/utils.ts b/packages/beacon-node/src/execution/engine/utils.ts index 1a88edb22cce..7e07ea8a5680 100644 --- a/packages/beacon-node/src/execution/engine/utils.ts +++ b/packages/beacon-node/src/execution/engine/utils.ts @@ -66,6 +66,7 @@ function getExecutionEngineStateForPayloadStatus(payloadStatus: ExecutionPayload case ExecutionPayloadStatus.INVALID: case ExecutionPayloadStatus.SYNCING: case ExecutionPayloadStatus.INVALID_BLOCK_HASH: + case ExecutionPayloadStatus.INVALID_INCLUSION_LIST: return ExecutionEngineState.SYNCING; case ExecutionPayloadStatus.UNAVAILABLE: diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index 461f9c9e935b..21a30942e39d 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -3,6 +3,7 @@ import {BeaconState} from "@lodestar/types"; import {BlobsSource, BlockSource} from "../../chain/blocks/types.js"; import {JobQueueItemType} from "../../chain/bls/index.js"; import {BlockErrorCode} from "../../chain/errors/index.js"; +import {InclusionListInsertOutcome} from "../../chain/opPools/inclusionListPool.js"; import {InsertOutcome} from "../../chain/opPools/types.js"; import {RegenCaller, RegenFnName} from "../../chain/regen/interface.js"; import {ReprocessStatus} from "../../chain/reprocess.js"; @@ -889,6 +890,15 @@ export function createLodestarMetrics( name: "lodestar_oppool_sync_contribution_and_proof_pool_pool_size", help: "Current size of the SyncContributionAndProofPool unique by slot subnet and block root", }), + inclusionListPoolSize: register.gauge({ + name: "lodestar_oppool_inclusion_list_pool_size", + help: "Current size of the InclusionListPool = total inclusion lists unique by validator and slot", + }), + inclusionListPoolInsertOutcome: register.counter<{insertOutcome: InclusionListInsertOutcome}>({ + name: "lodestar_inclusion_list_pool_insert_outcome_total", + help: "Total number of InsertOutcome as a result of adding an inclusion list in a pool", + labelNames: ["insertOutcome"], + }), }, // Validator monitoring diff --git a/packages/beacon-node/src/network/gossip/interface.ts b/packages/beacon-node/src/network/gossip/interface.ts index 93e9e3983d38..622041d13ed5 100644 --- a/packages/beacon-node/src/network/gossip/interface.ts +++ b/packages/beacon-node/src/network/gossip/interface.ts @@ -13,6 +13,7 @@ import { altair, capella, deneb, + eip7805, phase0, } from "@lodestar/types"; import {Logger} from "@lodestar/utils"; @@ -35,6 +36,7 @@ export enum GossipType { light_client_finality_update = "light_client_finality_update", light_client_optimistic_update = "light_client_optimistic_update", bls_to_execution_change = "bls_to_execution_change", + inclusion_list = "inclusion_list", } export type SequentialGossipType = Exclude; @@ -68,6 +70,7 @@ export type GossipTopicTypeMap = { [GossipType.light_client_finality_update]: {type: GossipType.light_client_finality_update}; [GossipType.light_client_optimistic_update]: {type: GossipType.light_client_optimistic_update}; [GossipType.bls_to_execution_change]: {type: GossipType.bls_to_execution_change}; + [GossipType.inclusion_list]: {type: GossipType.inclusion_list}; }; export type GossipTopicMap = { @@ -96,6 +99,7 @@ export type GossipTypeMap = { [GossipType.light_client_finality_update]: LightClientFinalityUpdate; [GossipType.light_client_optimistic_update]: LightClientOptimisticUpdate; [GossipType.bls_to_execution_change]: capella.SignedBLSToExecutionChange; + [GossipType.inclusion_list]: eip7805.SignedInclusionList; }; export type GossipFnByType = { diff --git a/packages/beacon-node/src/network/gossip/scoringParameters.ts b/packages/beacon-node/src/network/gossip/scoringParameters.ts index 890fd5acd251..750ef338a799 100644 --- a/packages/beacon-node/src/network/gossip/scoringParameters.ts +++ b/packages/beacon-node/src/network/gossip/scoringParameters.ts @@ -6,7 +6,7 @@ import { } from "@chainsafe/libp2p-gossipsub/score"; import {BeaconConfig} from "@lodestar/config"; import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, TARGET_AGGREGATORS_PER_COMMITTEE} from "@lodestar/params"; -import {computeCommitteeCount} from "@lodestar/state-transition"; +import {computeBeaconCommitteeCount} from "@lodestar/state-transition"; import {getActiveForks} from "../forks.js"; import {Eth2Context, Eth2GossipsubModules} from "./gossipsub.js"; import {GossipType} from "./interface.js"; @@ -301,7 +301,7 @@ function expectedAggregatorCountPerSlot(activeValidatorCount: number): { aggregatorsPerslot: number; committeesPerSlot: number; } { - const committeesPerSlot = computeCommitteeCount(activeValidatorCount); + const committeesPerSlot = computeBeaconCommitteeCount(activeValidatorCount); const committeesPerEpoch = committeesPerSlot * SLOTS_PER_EPOCH; const smallerCommitteeSize = Math.floor(activeValidatorCount / committeesPerEpoch); const largerCommiteeeSize = smallerCommitteeSize + 1; diff --git a/packages/beacon-node/src/network/gossip/topic.ts b/packages/beacon-node/src/network/gossip/topic.ts index bf44dd90b8af..fb60ac39e78b 100644 --- a/packages/beacon-node/src/network/gossip/topic.ts +++ b/packages/beacon-node/src/network/gossip/topic.ts @@ -69,6 +69,7 @@ function stringifyGossipTopicType(topic: GossipTopic): string { case GossipType.light_client_finality_update: case GossipType.light_client_optimistic_update: case GossipType.bls_to_execution_change: + case GossipType.inclusion_list: return topic.type; case GossipType.beacon_attestation: case GossipType.sync_committee: @@ -109,6 +110,8 @@ export function getGossipSSZType(topic: GossipTopic) { : ssz.altair.LightClientFinalityUpdate; case GossipType.bls_to_execution_change: return ssz.capella.SignedBLSToExecutionChange; + case GossipType.inclusion_list: + return ssz.eip7805.SignedInclusionList; } } @@ -185,6 +188,7 @@ export function parseGossipTopic(forkDigestContext: ForkDigestContext, topicStr: case GossipType.light_client_finality_update: case GossipType.light_client_optimistic_update: case GossipType.bls_to_execution_change: + case GossipType.inclusion_list: return {type: gossipTypeStr, fork, encoding}; } @@ -228,6 +232,10 @@ export function getCoreTopicsAtFork( {type: GossipType.attester_slashing}, ]; + if (ForkSeq[fork] >= ForkSeq.eip7805) { + topics.push({type: GossipType.inclusion_list}); + } + // After Deneb also track blob_sidecar_{subnet_id} if (ForkSeq[fork] >= ForkSeq.deneb) { const subnetCount = isForkPostElectra(fork) @@ -294,4 +302,5 @@ export const gossipTopicIgnoreDuplicatePublishError: Record [GossipType.light_client_finality_update]: false, [GossipType.light_client_optimistic_update]: false, [GossipType.bls_to_execution_change]: true, + [GossipType.inclusion_list]: true, }; diff --git a/packages/beacon-node/src/network/interface.ts b/packages/beacon-node/src/network/interface.ts index aab3c06991d3..1c8a1854d376 100644 --- a/packages/beacon-node/src/network/interface.ts +++ b/packages/beacon-node/src/network/interface.ts @@ -27,6 +27,7 @@ import { altair, capella, deneb, + eip7805, phase0, } from "@lodestar/types"; import type {Datastore} from "interface-datastore"; @@ -84,6 +85,7 @@ export interface INetwork extends INetworkCorePublic { publishContributionAndProof(contributionAndProof: altair.SignedContributionAndProof): Promise; publishLightClientFinalityUpdate(update: LightClientFinalityUpdate): Promise; publishLightClientOptimisticUpdate(update: LightClientOptimisticUpdate): Promise; + publishInclusionList(inclusionList: eip7805.SignedInclusionList): Promise; // Debug dumpGossipQueue(gossipType: GossipType): Promise; diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 505da6718c33..72756ac28ded 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -22,6 +22,7 @@ import { altair, capella, deneb, + eip7805, phase0, } from "@lodestar/types"; import {sleep} from "@lodestar/utils"; @@ -413,6 +414,15 @@ export class Network implements INetwork { ); } + async publishInclusionList(inclusionList: eip7805.SignedInclusionList): Promise { + const fork = this.config.getForkName(inclusionList.message.slot); + return this.publishGossip( + {type: GossipType.inclusion_list, fork}, + inclusionList, + {ignoreDuplicatePublishError: true} // TODO EIP-7805: Double check if we want to ignore duplicate error + ); + } + private async publishGossip( topic: GossipTopicMap[K], object: GossipTypeMap[K], @@ -519,6 +529,18 @@ export class Network implements INetwork { ); } + // TODO EIP-7805: add caller to this function + async sendInclusionListByCommitteeIndices( + peerId: PeerIdStr, + request: eip7805.InclusionListByCommitteeIndicesRequest + ): Promise { + return collectMaxResponseTyped( + this.sendReqRespRequest(peerId, ReqRespMethod.InclusionListByCommitteeIndices, [Version.V1], request), + this.config.MAX_REQUEST_INCLUSION_LIST, + responseSszTypeByMethod[ReqRespMethod.InclusionListByCommitteeIndices] + ); + } + private sendReqRespRequest( peerId: PeerIdStr, method: ReqRespMethod, diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index ec61aa277d03..130b79d2efa8 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -44,6 +44,7 @@ import { validateGossipAttesterSlashing, validateGossipBlock, validateGossipBlsToExecutionChange, + validateGossipInclusionList, validateGossipProposerSlashing, validateGossipSyncCommittee, validateGossipVoluntaryExit, @@ -608,6 +609,32 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand chain.emitter.emit(routes.events.EventType.blsToExecutionChange, blsToExecutionChange); }, + + [GossipType.inclusion_list]: async ({ + gossipData, + topic, + seenTimestampSec, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; + const inclusionList = sszDeserialize(topic, serializedData); + // TODO EIP-7805: should we persist invalid ssz value? + await validateGossipInclusionList(chain, inclusionList); + + try { + const insertOutcome = chain.inclusionListPool.add(inclusionList); + metrics?.opPool.inclusionListPoolInsertOutcome.inc({insertOutcome}); + + const secFromSlot = chain.clock.secFromSlot(inclusionList.message.slot, seenTimestampSec); + chain.forkChoice.onInclusionList(inclusionList, secFromSlot); + } catch (e) { + logger.error("Error adding inclusionList to pool", {}, e as Error); + } + + chain.emitter.emit(routes.events.EventType.inclusionList, { + version: config.getForkName(inclusionList.message.slot), + data: inclusionList, + }); + }, }; } diff --git a/packages/beacon-node/src/network/processor/gossipQueues/index.ts b/packages/beacon-node/src/network/processor/gossipQueues/index.ts index 4958fd8a50e6..b4461672651f 100644 --- a/packages/beacon-node/src/network/processor/gossipQueues/index.ts +++ b/packages/beacon-node/src/network/processor/gossipQueues/index.ts @@ -62,6 +62,11 @@ const linearGossipQueueOpts: { type: QueueType.FIFO, dropOpts: {type: DropType.count, count: 1}, }, + [GossipType.inclusion_list]: { + maxLength: 8192, // TODO EIP-7805: Verify this number. Unsigned IL uncompressed should be 8192. + type: QueueType.FIFO, + dropOpts: {type: DropType.count, count: 1}, + }, }; const indexedGossipQueueOpts: { diff --git a/packages/beacon-node/src/network/processor/index.ts b/packages/beacon-node/src/network/processor/index.ts index 2b471034aee5..24c475d8dd1d 100644 --- a/packages/beacon-node/src/network/processor/index.ts +++ b/packages/beacon-node/src/network/processor/index.ts @@ -76,6 +76,7 @@ const executeGossipWorkOrderObj: Record = { [GossipType.sync_committee]: {}, [GossipType.light_client_finality_update]: {}, [GossipType.light_client_optimistic_update]: {}, + [GossipType.inclusion_list]: {}, }; const executeGossipWorkOrder = Object.keys(executeGossipWorkOrderObj) as (keyof typeof executeGossipWorkOrderObj)[]; diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/beacon-node/src/network/reqresp/handlers/index.ts index 85eb4a0136b2..ae7aec1dfcdd 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/index.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/index.ts @@ -56,6 +56,11 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh }, [ReqRespMethod.LightClientFinalityUpdate]: () => onLightClientFinalityUpdate(chain), [ReqRespMethod.LightClientOptimisticUpdate]: () => onLightClientOptimisticUpdate(chain), + [ReqRespMethod.InclusionListByCommitteeIndices]: (req) => { + const _body = ssz.eip7805.InclusionListByCommitteeIndicesRequest.deserialize(req.data); + // TODO EIP-7805: Implement this + throw Error("InclusionListByCommitteeIndices is not implemented"); + }, }; return (method) => handlers[method]; diff --git a/packages/beacon-node/src/network/reqresp/rateLimit.ts b/packages/beacon-node/src/network/reqresp/rateLimit.ts index 7553310b96d7..983b8f5c52fe 100644 --- a/packages/beacon-node/src/network/reqresp/rateLimit.ts +++ b/packages/beacon-node/src/network/reqresp/rateLimit.ts @@ -63,6 +63,10 @@ export const rateLimitQuotas: (config: BeaconConfig) => Record = (fork: ForkName, version: number) => Type; @@ -115,6 +120,7 @@ export const responseSszTypeByMethod: {[K in ReqRespMethod]: ResponseTypeGetter< [ReqRespMethod.LightClientFinalityUpdate]: (fork) => sszTypesFor(onlyLightclientFork(fork)).LightClientFinalityUpdate, [ReqRespMethod.LightClientOptimisticUpdate]: (fork) => sszTypesFor(onlyLightclientFork(fork)).LightClientOptimisticUpdate, + [ReqRespMethod.InclusionListByCommitteeIndices]: () => ssz.eip7805.SignedInclusionList, }; function onlyLightclientFork(fork: ForkName): ForkLightClient { diff --git a/packages/beacon-node/test/e2e/api/impl/beacon/state/endpoint.test.ts b/packages/beacon-node/test/e2e/api/impl/beacon/state/endpoint.test.ts index 1b2c6b902f20..dff51a71adfc 100644 --- a/packages/beacon-node/test/e2e/api/impl/beacon/state/endpoint.test.ts +++ b/packages/beacon-node/test/e2e/api/impl/beacon/state/endpoint.test.ts @@ -2,7 +2,7 @@ import {ApiClient, getClient} from "@lodestar/api"; import {createBeaconConfig} from "@lodestar/config"; import {chainConfig as chainConfigDef} from "@lodestar/config/default"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; -import {computeCommitteeCount} from "@lodestar/state-transition"; +import {computeBeaconCommitteeCount} from "@lodestar/state-transition"; import {afterAll, beforeAll, describe, expect, it} from "vitest"; import {BeaconNode} from "../../../../../../src/node/nodejs.js"; import {LogLevel, testLogger} from "../../../../../utils/logger.js"; @@ -12,7 +12,7 @@ describe("beacon state api", () => { const restPort = 9596; const config = createBeaconConfig(chainConfigDef, Buffer.alloc(32, 0xaa)); const validatorCount = 512; - const committeesPerSlot = computeCommitteeCount(validatorCount); + const committeesPerSlot = computeBeaconCommitteeCount(validatorCount); const committeeCount = committeesPerSlot * SLOTS_PER_EPOCH; const validatorsPerCommittee = validatorCount / committeeCount; diff --git a/packages/beacon-node/test/e2e/network/gossipsub.test.ts b/packages/beacon-node/test/e2e/network/gossipsub.test.ts index e63b26d026db..59e4e9777ec8 100644 --- a/packages/beacon-node/test/e2e/network/gossipsub.test.ts +++ b/packages/beacon-node/test/e2e/network/gossipsub.test.ts @@ -40,6 +40,9 @@ function runTests({useWorker}: {useWorker: boolean}): void { ALTAIR_FORK_EPOCH: 1, BELLATRIX_FORK_EPOCH: 1, CAPELLA_FORK_EPOCH: 1, + DENEB_FORK_EPOCH: 1, + ELECTRA_FORK_EPOCH: 1, + EIP7805_FORK_EPOCH: 1, }); const START_SLOT = computeStartSlotAtEpoch(config.ALTAIR_FORK_EPOCH); @@ -233,13 +236,13 @@ function runTests({useWorker}: {useWorker: boolean}): void { } } - const lightClientOptimisticUpdate = ssz.capella.LightClientOptimisticUpdate.defaultValue(); + const lightClientOptimisticUpdate = ssz.electra.LightClientOptimisticUpdate.defaultValue(); lightClientOptimisticUpdate.signatureSlot = START_SLOT; await netA.publishLightClientOptimisticUpdate(lightClientOptimisticUpdate); const optimisticUpdate = await onLightClientOptimisticUpdatePromise; expect(Buffer.from(optimisticUpdate)).toEqual( - Buffer.from(ssz.capella.LightClientOptimisticUpdate.serialize(lightClientOptimisticUpdate)) + Buffer.from(ssz.electra.LightClientOptimisticUpdate.serialize(lightClientOptimisticUpdate)) ); }); @@ -272,15 +275,50 @@ function runTests({useWorker}: {useWorker: boolean}): void { } } - const lightClientFinalityUpdate = ssz.capella.LightClientFinalityUpdate.defaultValue(); + const lightClientFinalityUpdate = ssz.electra.LightClientFinalityUpdate.defaultValue(); lightClientFinalityUpdate.signatureSlot = START_SLOT; await netA.publishLightClientFinalityUpdate(lightClientFinalityUpdate); const optimisticUpdate = await onLightClientFinalityUpdatePromise; expect(Buffer.from(optimisticUpdate)).toEqual( - Buffer.from(ssz.capella.LightClientFinalityUpdate.serialize(lightClientFinalityUpdate)) + Buffer.from(ssz.electra.LightClientFinalityUpdate.serialize(lightClientFinalityUpdate)) ); }); + + it("Publish and receive an inclusion list", async () => { + let onInclusionList: (fu: Uint8Array) => void; + const onInclusionListPromise = new Promise((resolve) => { + onInclusionList = resolve; + }); + + const {netA, netB} = await mockModules({ + [GossipType.inclusion_list]: async ({gossipData}: GossipHandlerParamGeneric) => { + onInclusionList(gossipData.serializedData); + }, + }); + + await Promise.all([onPeerConnect(netA), onPeerConnect(netB), connect(netA, netB)]); + expect(netA.getConnectedPeerCount()).toBe(1); + expect(netB.getConnectedPeerCount()).toBe(1); + + await netA.subscribeGossipCoreTopics(); + await netB.subscribeGossipCoreTopics(); + + // Wait to have a peer connected to a topic + while (!netA.closed) { + await sleep(500); + if (await hasSomeMeshPeer(netA)) { + break; + } + } + + const inclusionList = ssz.eip7805.SignedInclusionList.defaultValue(); + inclusionList.message.slot = START_SLOT; + await netA.publishInclusionList(inclusionList); + + const received = await onInclusionListPromise; + expect(Buffer.from(received)).toEqual(Buffer.from(ssz.eip7805.SignedInclusionList.serialize(inclusionList))); + }); } async function hasSomeMeshPeer(net: Network): Promise { 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 28c1f198da28..68c685fa4873 100644 --- a/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts @@ -67,6 +67,7 @@ describe(`getAttestationsForBlock vc=${vc}`, () => { executionStatus: ExecutionStatus.PreMerge, timeliness: false, + isEip7805Enabled: false, dataAvailabilityStatus: DataAvailabilityStatus.PreData, }, originalState.slot @@ -92,6 +93,7 @@ describe(`getAttestationsForBlock vc=${vc}`, () => { executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge, timeliness: false, + isEip7805Enabled: false, dataAvailabilityStatus: DataAvailabilityStatus.PreData, }, slot diff --git a/packages/beacon-node/test/spec/presets/fork.test.ts b/packages/beacon-node/test/spec/presets/fork.test.ts index bc66bccb279e..ae41c84dabf1 100644 --- a/packages/beacon-node/test/spec/presets/fork.test.ts +++ b/packages/beacon-node/test/spec/presets/fork.test.ts @@ -38,6 +38,9 @@ const fork: TestRunnerFn = (forkNext) => { return slotFns.upgradeStateToDeneb(preState as CachedBeaconStateCapella); case ForkName.electra: return slotFns.upgradeStateToElectra(preState as CachedBeaconStateDeneb); + case ForkName.eip7805: + // TODO EIP-7805: likely not needed, there are no state changes + throw Error(`Unsupported fork ${forkNext}`); } }, options: { diff --git a/packages/beacon-node/test/spec/presets/transition.test.ts b/packages/beacon-node/test/spec/presets/transition.test.ts index 0b1aa1b522e5..a5dd19d850ae 100644 --- a/packages/beacon-node/test/spec/presets/transition.test.ts +++ b/packages/beacon-node/test/spec/presets/transition.test.ts @@ -108,6 +108,15 @@ function getTransitionConfig(fork: ForkName, forkEpoch: number): Partial { bellatrix: ssz.altair.LightClientHeader.defaultValue(), deneb: ssz.deneb.LightClientHeader.defaultValue(), electra: ssz.deneb.LightClientHeader.defaultValue(), + eip7805: ssz.deneb.LightClientHeader.defaultValue(), }; testSlots = { @@ -37,6 +38,7 @@ describe("UpgradeLightClientHeader", () => { capella: 25, deneb: 33, electra: 41, + eip7805: 49, }; }); diff --git a/packages/beacon-node/test/unit/chain/stateCache/blockStateCacheImpl.test.ts b/packages/beacon-node/test/unit/chain/stateCache/blockStateCacheImpl.test.ts index 50ec0b6fc63d..8314363b47f0 100644 --- a/packages/beacon-node/test/unit/chain/stateCache/blockStateCacheImpl.test.ts +++ b/packages/beacon-node/test/unit/chain/stateCache/blockStateCacheImpl.test.ts @@ -14,8 +14,9 @@ describe("BlockStateCacheImpl", () => { epoch: 0, activeIndices: new Uint32Array(), shuffling: new Uint32Array(), - committees: [], - committeesPerSlot: 1, + beaconCommittees: [], + beaconCommitteesPerSlot: 1, + inclusionListCommittees: [], }; beforeEach(() => { 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 9a341438479d..00f6c3bf616b 100644 --- a/packages/beacon-node/test/unit/chain/stateCache/fifoBlockStateCache.test.ts +++ b/packages/beacon-node/test/unit/chain/stateCache/fifoBlockStateCache.test.ts @@ -11,8 +11,9 @@ describe("FIFOBlockStateCache", () => { epoch: 0, activeIndices: new Uint32Array(), shuffling: new Uint32Array(), - committees: [], - committeesPerSlot: 1, + beaconCommittees: [], + beaconCommitteesPerSlot: 1, + inclusionListCommittees: [], }; const state1 = generateCachedState({slot: 0}); diff --git a/packages/beacon-node/test/unit/network/fork.test.ts b/packages/beacon-node/test/unit/network/fork.test.ts index ed11ce979ef0..d27f8e3155f6 100644 --- a/packages/beacon-node/test/unit/network/fork.test.ts +++ b/packages/beacon-node/test/unit/network/fork.test.ts @@ -10,6 +10,7 @@ function getForkConfig({ capella, deneb, electra, + eip7805, }: { phase0: number; altair: number; @@ -17,6 +18,7 @@ function getForkConfig({ capella: number; deneb: number; electra: number; + eip7805: number; }): BeaconConfig { const forks: Record = { phase0: { @@ -67,6 +69,14 @@ function getForkConfig({ prevVersion: Buffer.from([0, 0, 0, 4]), prevForkName: ForkName.deneb, }, + eip7805: { + name: ForkName.eip7805, + seq: ForkSeq.eip7805, + epoch: eip7805, + version: Buffer.from([0, 0, 0, 6]), + prevVersion: Buffer.from([0, 0, 0, 5]), + prevForkName: ForkName.electra, + }, }; const forksAscendingEpochOrder = Object.values(forks); const forksDescendingEpochOrder = Object.values(forks).reverse(); @@ -144,9 +154,10 @@ for (const testScenario of testScenarios) { const {phase0, altair, bellatrix, capella, testCases} = testScenario; const deneb = Infinity; const electra = Infinity; + const eip7805 = Infinity; describe(`network / fork: phase0: ${phase0}, altair: ${altair}, bellatrix: ${bellatrix} capella: ${capella}`, () => { - const forkConfig = getForkConfig({phase0, altair, bellatrix, capella, deneb, electra}); + const forkConfig = getForkConfig({phase0, altair, bellatrix, capella, deneb, electra, eip7805}); const forks = forkConfig.forks; for (const testCase of testCases) { const {epoch, currentFork, nextFork, activeForks} = testCase; diff --git a/packages/beacon-node/test/unit/network/gossip/topic.test.ts b/packages/beacon-node/test/unit/network/gossip/topic.test.ts index 4b323865061e..c94e7c163dfe 100644 --- a/packages/beacon-node/test/unit/network/gossip/topic.test.ts +++ b/packages/beacon-node/test/unit/network/gossip/topic.test.ts @@ -81,6 +81,13 @@ describe("network / gossip / topic", () => { topicStr: "/eth2/8e04f66f/light_client_optimistic_update/ssz_snappy", }, ], + [GossipType.inclusion_list]: [ + { + topic: {type: GossipType.inclusion_list, fork: ForkName.eip7805, encoding}, + // TODO EIP-7805: this is not correct + topicStr: "/eth2/46acb19a/inclusion_list/ssz_snappy", + }, + ], }; for (const topics of Object.values(testCases)) { diff --git a/packages/beacon-node/test/utils/config.ts b/packages/beacon-node/test/utils/config.ts index 6822b546de1e..cfd1e6a83bab 100644 --- a/packages/beacon-node/test/utils/config.ts +++ b/packages/beacon-node/test/utils/config.ts @@ -38,5 +38,14 @@ export function getConfig(fork: ForkName, forkEpoch = 0): ChainForkConfig { DENEB_FORK_EPOCH: 0, ELECTRA_FORK_EPOCH: forkEpoch, }); + case ForkName.eip7805: + return createChainForkConfig({ + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: 0, + EIP7805_FORK_EPOCH: forkEpoch, + }); } } diff --git a/packages/beacon-node/test/utils/state.ts b/packages/beacon-node/test/utils/state.ts index 3e14ccf7630b..7ab7169b16f8 100644 --- a/packages/beacon-node/test/utils/state.ts +++ b/packages/beacon-node/test/utils/state.ts @@ -176,6 +176,7 @@ export const zeroProtoBlock: ProtoBlock = { unrealizedFinalizedRoot: ZERO_HASH_HEX, timeliness: false, + isEip7805Enabled: false, ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, dataAvailabilityStatus: DataAvailabilityStatus.PreData, diff --git a/packages/beacon-node/test/utils/typeGenerator.ts b/packages/beacon-node/test/utils/typeGenerator.ts index 329060e6eb31..7cfeed391758 100644 --- a/packages/beacon-node/test/utils/typeGenerator.ts +++ b/packages/beacon-node/test/utils/typeGenerator.ts @@ -41,6 +41,7 @@ export function generateProtoBlock(overrides: Partial = {}): ProtoBl unrealizedFinalizedRoot: ZERO_HASH_HEX, timeliness: false, + isEip7805Enabled: false, ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, dataAvailabilityStatus: DataAvailabilityStatus.PreData, diff --git a/packages/beacon-node/test/utils/validationData/attestation.ts b/packages/beacon-node/test/utils/validationData/attestation.ts index e8f0fca632eb..1ed3d247bb6c 100644 --- a/packages/beacon-node/test/utils/validationData/attestation.ts +++ b/packages/beacon-node/test/utils/validationData/attestation.ts @@ -70,6 +70,7 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { unrealizedFinalizedRoot: ZERO_HASH_HEX, timeliness: false, + isEip7805Enabled: false, ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, dataAvailabilityStatus: DataAvailabilityStatus.PreData, diff --git a/packages/config/src/chainConfig/configs/mainnet.ts b/packages/config/src/chainConfig/configs/mainnet.ts index 7b332f94b1a6..757740c3de7b 100644 --- a/packages/config/src/chainConfig/configs/mainnet.ts +++ b/packages/config/src/chainConfig/configs/mainnet.ts @@ -50,7 +50,11 @@ export const chainConfig: ChainConfig = { // ELECTRA ELECTRA_FORK_VERSION: b("0x05000000"), - ELECTRA_FORK_EPOCH: Infinity, + ELECTRA_FORK_EPOCH: 1000000, // Arbitrary value served as placeholder + + // EIP-7805 + EIP7805_FORK_VERSION: b("0x06000000"), + EIP7805_FORK_EPOCH: Infinity, // Time parameters // --------------------------------------------------------------- @@ -64,6 +68,9 @@ export const chainConfig: ChainConfig = { SHARD_COMMITTEE_PERIOD: 256, // 2**11 (= 2,048) Eth1 blocks ~8 hours ETH1_FOLLOW_DISTANCE: 2048, + ATTESTATION_DEADLINE: 4, + PROPOSER_INCLUSION_LIST_CUT_OFF: 11, + VIEW_FREEZE_DEADLINE: 9, // Validator cycle // --------------------------------------------------------------- @@ -115,4 +122,10 @@ export const chainConfig: ChainConfig = { MAX_BLOBS_PER_BLOCK_ELECTRA: 9, // MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152, + + // EIP-7805 + // 2**4 (= 16) + MAX_REQUEST_INCLUSION_LIST: 16, + // 2**13 (=8192) + MAX_BYTES_PER_INCLUSION_LIST: 8192, }; diff --git a/packages/config/src/chainConfig/configs/minimal.ts b/packages/config/src/chainConfig/configs/minimal.ts index 0902742277e0..02bea06bde8e 100644 --- a/packages/config/src/chainConfig/configs/minimal.ts +++ b/packages/config/src/chainConfig/configs/minimal.ts @@ -47,6 +47,9 @@ export const chainConfig: ChainConfig = { // ELECTRA ELECTRA_FORK_VERSION: b("0x05000001"), ELECTRA_FORK_EPOCH: Infinity, + // EIP-7805 + EIP7805_FORK_VERSION: b("0x06000001"), + EIP7805_FORK_EPOCH: Infinity, // Time parameters // --------------------------------------------------------------- @@ -60,6 +63,9 @@ export const chainConfig: ChainConfig = { SHARD_COMMITTEE_PERIOD: 64, // [customized] process deposits more quickly, but insecure ETH1_FOLLOW_DISTANCE: 16, + ATTESTATION_DEADLINE: 2, + PROPOSER_INCLUSION_LIST_CUT_OFF: 5, + VIEW_FREEZE_DEADLINE: 3, // Validator cycle // --------------------------------------------------------------- @@ -112,4 +118,10 @@ export const chainConfig: ChainConfig = { MAX_BLOBS_PER_BLOCK_ELECTRA: 9, // MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_ELECTRA MAX_REQUEST_BLOB_SIDECARS_ELECTRA: 1152, + + // EIP-7805 + // 2**4 (= 16) + MAX_REQUEST_INCLUSION_LIST: 16, + // 2**13 (=8192) + MAX_BYTES_PER_INCLUSION_LIST: 8192, }; diff --git a/packages/config/src/chainConfig/types.ts b/packages/config/src/chainConfig/types.ts index 8ca338c89a24..20a570bc085a 100644 --- a/packages/config/src/chainConfig/types.ts +++ b/packages/config/src/chainConfig/types.ts @@ -41,6 +41,9 @@ export type ChainConfig = { // ELECTRA ELECTRA_FORK_VERSION: Uint8Array; ELECTRA_FORK_EPOCH: number; + // EIP-7805 + EIP7805_FORK_VERSION: Uint8Array; + EIP7805_FORK_EPOCH: number; // Time parameters SECONDS_PER_SLOT: number; @@ -48,6 +51,9 @@ export type ChainConfig = { MIN_VALIDATOR_WITHDRAWABILITY_DELAY: number; SHARD_COMMITTEE_PERIOD: number; ETH1_FOLLOW_DISTANCE: number; + ATTESTATION_DEADLINE: number; + PROPOSER_INCLUSION_LIST_CUT_OFF: number; + VIEW_FREEZE_DEADLINE: number; // Validator cycle INACTIVITY_SCORE_BIAS: number; @@ -78,6 +84,10 @@ export type ChainConfig = { BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: number; MAX_BLOBS_PER_BLOCK_ELECTRA: number; MAX_REQUEST_BLOB_SIDECARS_ELECTRA: number; + + // EIP-7805 + MAX_REQUEST_INCLUSION_LIST: number; + MAX_BYTES_PER_INCLUSION_LIST: number; }; export const chainConfigTypes: SpecTypes = { @@ -111,6 +121,9 @@ export const chainConfigTypes: SpecTypes = { // ELECTRA ELECTRA_FORK_VERSION: "bytes", ELECTRA_FORK_EPOCH: "number", + // EIP-7805 + EIP7805_FORK_VERSION: "bytes", + EIP7805_FORK_EPOCH: "number", // Time parameters SECONDS_PER_SLOT: "number", @@ -118,6 +131,9 @@ export const chainConfigTypes: SpecTypes = { MIN_VALIDATOR_WITHDRAWABILITY_DELAY: "number", SHARD_COMMITTEE_PERIOD: "number", ETH1_FOLLOW_DISTANCE: "number", + ATTESTATION_DEADLINE: "number", + PROPOSER_INCLUSION_LIST_CUT_OFF: "number", + VIEW_FREEZE_DEADLINE: "number", // Validator cycle INACTIVITY_SCORE_BIAS: "number", @@ -148,6 +164,10 @@ export const chainConfigTypes: SpecTypes = { BLOB_SIDECAR_SUBNET_COUNT_ELECTRA: "number", MAX_BLOBS_PER_BLOCK_ELECTRA: "number", MAX_REQUEST_BLOB_SIDECARS_ELECTRA: "number", + + // EIP-7805 + MAX_REQUEST_INCLUSION_LIST: "number", + MAX_BYTES_PER_INCLUSION_LIST: "number", }; /** Allows values in a Spec file */ diff --git a/packages/config/src/forkConfig/index.ts b/packages/config/src/forkConfig/index.ts index 9551627188d5..fd8536322f67 100644 --- a/packages/config/src/forkConfig/index.ts +++ b/packages/config/src/forkConfig/index.ts @@ -68,10 +68,18 @@ export function createForkConfig(config: ChainConfig): ForkConfig { prevVersion: config.DENEB_FORK_VERSION, prevForkName: ForkName.deneb, }; + const eip7805: ForkInfo = { + name: ForkName.eip7805, + seq: ForkSeq.eip7805, + epoch: config.EIP7805_FORK_EPOCH, + version: config.EIP7805_FORK_VERSION, + prevVersion: config.ELECTRA_FORK_VERSION, + prevForkName: ForkName.electra, + }; /** Forks in order order of occurence, `phase0` first */ // Note: Downstream code relies on proper ordering. - const forks = {phase0, altair, bellatrix, capella, deneb, electra}; + const forks = {phase0, altair, bellatrix, capella, deneb, electra, eip7805}; // Prevents allocating an array on every getForkInfo() call const forksAscendingEpochOrder = Object.values(forks); diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 2b803c3553a4..4dc70332a7d7 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -1,5 +1,5 @@ import {ChainConfig, ChainForkConfig} from "@lodestar/config"; -import {INTERVALS_PER_SLOT, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; +import {INTERVALS_PER_SLOT, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT, isForkPostEip7805} from "@lodestar/params"; import { CachedBeaconStateAllForks, EffectiveBalanceIncrements, @@ -13,7 +13,18 @@ import { isExecutionStateType, } from "@lodestar/state-transition"; import {computeUnrealizedCheckpoints} from "@lodestar/state-transition/epoch"; -import {BeaconBlock, Epoch, Root, RootHex, Slot, ValidatorIndex, bellatrix, phase0, ssz} from "@lodestar/types"; +import { + BeaconBlock, + Epoch, + Root, + RootHex, + Slot, + ValidatorIndex, + bellatrix, + eip7805, + phase0, + ssz, +} from "@lodestar/types"; import {Logger, MapDef, fromHex, toRootHex} from "@lodestar/utils"; import {computeDeltas} from "../protoArray/computeDeltas.js"; @@ -41,7 +52,13 @@ import { NotReorgedReason, PowBlockHex, } from "./interface.js"; -import {CheckpointWithHex, IForkChoiceStore, JustifiedBalances, toCheckpointWithHex} from "./store.js"; +import { + CheckpointWithHex, + IForkChoiceStore, + InclusionListStoreKey, + JustifiedBalances, + toCheckpointWithHex, +} from "./store.js"; export type ForkChoiceOpts = { proposerBoost?: boolean; @@ -175,6 +192,25 @@ export class ForkChoice implements IForkChoice { return this.head; } + /** + * EIP-7805: Get attester's head + * See `get_attester_head()` + */ + // TODO EIP-7805: Can getAttesterHead return unsatisified block when predictProposerHead calls? + getAttesterHead(): ProtoBlock { + const head = this.getHead(); + const headRoot = head.blockRoot; + + if (!head.isEip7805Enabled) { + return head; + } + + // Attempt to return parent block if head is IL unsatisified + return this.fcStore.unsatisifiedInclusionListBlocks.has(headRoot) + ? (this.protoArray.getBlock(head.parentRoot) ?? head) + : head; + } + /** * * A multiplexer to wrap around the traditional `updateHead()` according to the scenario @@ -193,22 +229,27 @@ export class ForkChoice implements IForkChoice { const {mode} = opt; const canonicialHeadBlock = mode === UpdateHeadOpt.GetPredictedProposerHead ? this.getHead() : this.updateHead(); + let result; switch (mode) { case UpdateHeadOpt.GetPredictedProposerHead: - return {head: this.predictProposerHead(canonicialHeadBlock, opt.slot)}; + result = {head: this.predictProposerHead(canonicialHeadBlock, opt.slot)}; + break; case UpdateHeadOpt.GetProposerHead: { const { proposerHead: head, isHeadTimely, notReorgedReason, } = this.getProposerHead(canonicialHeadBlock, opt.secFromSlot, opt.slot); - return {head, isHeadTimely, notReorgedReason}; + result = {head, isHeadTimely, notReorgedReason}; + break; } case UpdateHeadOpt.GetCanonicialHead: - return {head: canonicialHeadBlock}; default: - return {head: canonicialHeadBlock}; + result = {head: canonicialHeadBlock}; + break; } + + return result; } /** @@ -551,6 +592,9 @@ export class ForkChoice implements IForkChoice { this.proposerBoostRoot = blockRootHex; } + // Communicate if EIP-7805 is enabled + const isEip7805Enabled = isForkPostEip7805(this.config.getForkName(currentSlot)); + // As per specs, we should be validating here the terminal conditions of // the PoW if this were a merge transition block. // (https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/fork-choice.md#on_block) @@ -639,6 +683,7 @@ export class ForkChoice implements IForkChoice { targetRoot: toRootHex(targetRoot), stateRoot: toRootHex(block.stateRoot), timeliness: isTimely, + isEip7805Enabled, justifiedEpoch: stateJustifiedEpoch, justifiedRoot: toRootHex(state.currentJustifiedCheckpoint.root), @@ -746,6 +791,54 @@ export class ForkChoice implements IForkChoice { } } + // Skip all validation check that overlaps `validateInclusionList()` since an IL needs to pass it before calling `onInclusionList()` + onInclusionList(inclusionList: eip7805.SignedInclusionList, secFromSlot: number): void { + const currentSlot = this.fcStore.currentSlot; + const {slot, inclusionListCommitteeRoot, validatorIndex} = inclusionList.message; + + // If the inclusion list is from the previous slot, ignore it if already past the attestation deadline + const isBeforeAttestingInterval = secFromSlot >= Math.floor(this.config.SECONDS_PER_SLOT / INTERVALS_PER_SLOT); + if (slot === currentSlot - 1 && !isBeforeAttestingInterval) { + return; + } + + // TODO EIP-7805: Remove magic number + const isBeforeFreezeDeadline = slot === currentSlot && secFromSlot < 9; // VIEW_FREEZE_DEADLINE + + const equivocators = this.fcStore.inclusionListEquivocators.get([slot, inclusionListCommitteeRoot]); + + // Do not process inclusion lists from known equivocators + if (equivocators?.has(validatorIndex)) { + return; + } + + const storeKey: InclusionListStoreKey = [slot, inclusionListCommitteeRoot]; + const storedInclusionLists = this.fcStore.inclusionLists.get(storeKey) ?? []; + const validatorInclusionLists = storedInclusionLists.filter((il) => il.validatorIndex === validatorIndex); + + if (validatorInclusionLists.length > 0) { + const validatorInclusionList = validatorInclusionLists[0]; + + // TODO EIP-7805: Avoid using JSON.stringify to compare ILs + if (JSON.stringify(validatorInclusionList) !== JSON.stringify(inclusionList.message)) { + // We have equivocation evidence for `validator_index`, record it as equivocator + const equivocators = this.fcStore.inclusionListEquivocators.get(storeKey) ?? new Set(); + equivocators.add(validatorIndex); + this.fcStore.inclusionListEquivocators.set(storeKey, equivocators); + } + } else if (isBeforeFreezeDeadline) { + const inclusionLists = this.fcStore.inclusionLists.get(storeKey) ?? []; + inclusionLists.push(inclusionList.message); + this.fcStore.inclusionLists.set(storeKey, inclusionLists); + } + + return; + } + + addInclusionListUnsatisfiedBlock(blockRoot: RootHex) { + this.fcStore.unsatisifiedInclusionListBlocks.add(blockRoot); + } + getLatestMessage(validatorIndex: ValidatorIndex): LatestMessage | undefined { const vote = this.votes[validatorIndex]; if (vote === undefined) { diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index 05c803b50dc8..ab4bb617c51e 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -1,6 +1,16 @@ import {EffectiveBalanceIncrements} from "@lodestar/state-transition"; import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; -import {BeaconBlock, Epoch, IndexedAttestation, Root, RootHex, Slot, ValidatorIndex, phase0} from "@lodestar/types"; +import { + BeaconBlock, + Epoch, + IndexedAttestation, + Root, + RootHex, + Slot, + ValidatorIndex, + eip7805, + phase0, +} from "@lodestar/types"; import { DataAvailabilityStatus, LVHExecResponse, @@ -99,6 +109,7 @@ export interface IForkChoice { */ getHeadRoot(): RootHex; getHead(): ProtoBlock; + getAttesterHead(): ProtoBlock; updateAndGetHead(mode: UpdateAndGetHeadOpt): { head: ProtoBlock; isHeadTimely?: boolean; @@ -165,6 +176,10 @@ export interface IForkChoice { * https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/fork-choice.md#on_attester_slashing */ onAttesterSlashing(slashing: phase0.AttesterSlashing): void; + /** + * inclusionListCommittee is a list of IL committee validators' index in the current slot + */ + onInclusionList(inclusionList: eip7805.SignedInclusionList, secFromSlot: number): void; getLatestMessage(validatorIndex: ValidatorIndex): LatestMessage | undefined; /** * Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`. @@ -237,6 +252,11 @@ export interface IForkChoice { * A dependent root is the block root of the last block before the state transition that decided a specific shuffling */ getDependentRoot(block: ProtoBlock, atEpochDiff: EpochDifference): RootHex; + + /** + * Add IL-unsatisifed block root to store. + */ + addInclusionListUnsatisfiedBlock(blockRoot: RootHex): void; } /** Same to the PowBlock but we want RootHex to work with forkchoice conveniently */ diff --git a/packages/fork-choice/src/forkChoice/store.ts b/packages/fork-choice/src/forkChoice/store.ts index fda01689f96c..d7558ddc593b 100644 --- a/packages/fork-choice/src/forkChoice/store.ts +++ b/packages/fork-choice/src/forkChoice/store.ts @@ -1,5 +1,5 @@ import {CachedBeaconStateAllForks, EffectiveBalanceIncrements} from "@lodestar/state-transition"; -import {RootHex, Slot, ValidatorIndex, phase0} from "@lodestar/types"; +import {Root, RootHex, Slot, ValidatorIndex, eip7805, phase0} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; import {CheckpointHexWithBalance, CheckpointHexWithTotalBalance} from "./interface.js"; @@ -12,6 +12,12 @@ export type CheckpointWithHex = phase0.Checkpoint & {rootHex: RootHex}; export type JustifiedBalances = EffectiveBalanceIncrements; +export type InclusionListStoreKey = [Slot, Root]; +// TODO EIP-7805: Need prune mechanism to these three +class InclusionListStore extends Map {} +class InclusionListEquivocatorStore extends Map> {} +class InclusionListCommitteeRootStore extends Set {} + /** * Returns the justified balances of checkpoint. * MUST not throw an error in any case, related to cache miss. Either trigger regen or approximate from a close state. @@ -44,6 +50,9 @@ export interface IForkChoiceStore { unrealizedFinalizedCheckpoint: CheckpointWithHex; justifiedBalancesGetter: JustifiedBalancesGetter; equivocatingIndices: Set; + inclusionLists: InclusionListStore; + inclusionListEquivocators: InclusionListEquivocatorStore; + unsatisifiedInclusionListBlocks: InclusionListCommitteeRootStore; } /** @@ -57,6 +66,9 @@ export class ForkChoiceStore implements IForkChoiceStore { equivocatingIndices = new Set(); justifiedBalancesGetter: JustifiedBalancesGetter; currentSlot: Slot; + inclusionLists = new InclusionListStore(); + inclusionListEquivocators = new InclusionListEquivocatorStore(); + unsatisifiedInclusionListBlocks = new InclusionListCommitteeRootStore(); constructor( currentSlot: Slot, diff --git a/packages/fork-choice/src/protoArray/interface.ts b/packages/fork-choice/src/protoArray/interface.ts index 5e36593697f6..872c19ce4039 100644 --- a/packages/fork-choice/src/protoArray/interface.ts +++ b/packages/fork-choice/src/protoArray/interface.ts @@ -94,6 +94,9 @@ export type ProtoBlock = BlockExtraMeta & { // Indicate whether block arrives in a timely manner ie. before the 4 second mark timeliness: boolean; + + // Indicate whether EIP-7805 is enabled + isEip7805Enabled: boolean; }; /** diff --git a/packages/fork-choice/test/perf/forkChoice/util.ts b/packages/fork-choice/test/perf/forkChoice/util.ts index 4376ce047727..3630424b5ff3 100644 --- a/packages/fork-choice/test/perf/forkChoice/util.ts +++ b/packages/fork-choice/test/perf/forkChoice/util.ts @@ -85,6 +85,7 @@ export function initializeForkChoice(opts: Opts): ForkChoice { executionStatus: ExecutionStatus.PreMerge, timeliness: false, + isEip7805Enabled: false, dataAvailabilityStatus: DataAvailabilityStatus.PreData, }; diff --git a/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts b/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts index db7482f30cde..fd0cba4ec8e7 100644 --- a/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts +++ b/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts @@ -103,6 +103,7 @@ describe("Forkchoice", () => { executionStatus: ExecutionStatus.PreMerge, timeliness: false, + isEip7805Enabled: false, dataAvailabilityStatus: DataAvailabilityStatus.PreData, }; }; diff --git a/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts b/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts index ba225756733d..d1a0428ec6c9 100644 --- a/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts +++ b/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts @@ -46,6 +46,7 @@ describe("Forkchoice / GetProposerHead", () => { executionStatus: ExecutionStatus.PreMerge, timeliness: false, + isEip7805Enabled: false, dataAvailabilityStatus: DataAvailabilityStatus.PreData, }; @@ -69,6 +70,7 @@ describe("Forkchoice / GetProposerHead", () => { executionStatus: ExecutionStatus.PreMerge, timeliness: false, + isEip7805Enabled: false, weight: 29, dataAvailabilityStatus: DataAvailabilityStatus.PreData, @@ -94,6 +96,7 @@ describe("Forkchoice / GetProposerHead", () => { executionStatus: ExecutionStatus.PreMerge, timeliness: false, + isEip7805Enabled: false, weight: 212, // 240 - 29 + 1 dataAvailabilityStatus: DataAvailabilityStatus.PreData, }; diff --git a/packages/fork-choice/test/unit/protoArray/executionStatusUpdates.test.ts b/packages/fork-choice/test/unit/protoArray/executionStatusUpdates.test.ts index 4fcc6c79d190..6d14716b1812 100644 --- a/packages/fork-choice/test/unit/protoArray/executionStatusUpdates.test.ts +++ b/packages/fork-choice/test/unit/protoArray/executionStatusUpdates.test.ts @@ -113,6 +113,7 @@ function setupForkChoice(): ProtoArray { unrealizedFinalizedRoot: "-", timeliness: false, + isEip7805Enabled: false, ...executionData, }, diff --git a/packages/fork-choice/test/unit/protoArray/getCommonAncestor.test.ts b/packages/fork-choice/test/unit/protoArray/getCommonAncestor.test.ts index 3bc322112332..f256bffb4ee3 100644 --- a/packages/fork-choice/test/unit/protoArray/getCommonAncestor.test.ts +++ b/packages/fork-choice/test/unit/protoArray/getCommonAncestor.test.ts @@ -41,6 +41,7 @@ describe("getCommonAncestor", () => { unrealizedFinalizedRoot: "-", timeliness: false, + isEip7805Enabled: false, ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, dataAvailabilityStatus: DataAvailabilityStatus.PreData, @@ -67,6 +68,7 @@ describe("getCommonAncestor", () => { unrealizedFinalizedRoot: "-", timeliness: false, + isEip7805Enabled: false, ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, dataAvailabilityStatus: DataAvailabilityStatus.PreData, diff --git a/packages/fork-choice/test/unit/protoArray/protoArray.test.ts b/packages/fork-choice/test/unit/protoArray/protoArray.test.ts index 5e87504dc455..c49686bb7526 100644 --- a/packages/fork-choice/test/unit/protoArray/protoArray.test.ts +++ b/packages/fork-choice/test/unit/protoArray/protoArray.test.ts @@ -31,6 +31,7 @@ describe("ProtoArray", () => { unrealizedFinalizedRoot: stateRoot, timeliness: false, + isEip7805Enabled: false, ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, dataAvailabilityStatus: DataAvailabilityStatus.PreData, @@ -57,6 +58,7 @@ describe("ProtoArray", () => { unrealizedFinalizedRoot: stateRoot, timeliness: false, + isEip7805Enabled: false, ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, dataAvailabilityStatus: DataAvailabilityStatus.PreData, @@ -83,6 +85,7 @@ describe("ProtoArray", () => { unrealizedFinalizedRoot: stateRoot, timeliness: false, + isEip7805Enabled: false, ...{executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}, dataAvailabilityStatus: DataAvailabilityStatus.PreData, diff --git a/packages/params/src/forkName.ts b/packages/params/src/forkName.ts index 42e8917942d2..f5181e1b716b 100644 --- a/packages/params/src/forkName.ts +++ b/packages/params/src/forkName.ts @@ -8,6 +8,7 @@ export enum ForkName { capella = "capella", deneb = "deneb", electra = "electra", + eip7805 = "eip7805", } /** @@ -20,6 +21,7 @@ export enum ForkSeq { capella = 3, deneb = 4, electra = 5, + eip7805 = 6, } function exclude(coll: T[], val: U[]): Exclude[] { @@ -93,3 +95,17 @@ export const forkPostElectra = exclude(forkAll, [ export function isForkPostElectra(fork: ForkName): fork is ForkPostElectra { return isForkBlobs(fork) && fork !== ForkName.deneb; } + +export type ForkPreEip7805 = ForkPreElectra | ForkName.electra; +export type ForkPostEip7805 = Exclude; +export const forkPostEip7805 = exclude(forkAll, [ + ForkName.phase0, + ForkName.altair, + ForkName.bellatrix, + ForkName.capella, + ForkName.deneb, + ForkName.electra, +]); +export function isForkPostEip7805(fork: ForkName): fork is ForkPostEip7805 { + return isForkPostElectra(fork) && fork !== ForkName.electra; +} diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index 838624e29f23..d3bf25e9b642 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -146,6 +146,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_INCLUSION_LIST_COMMITTEE = Uint8Array.from([12, 0, 0, 0]); // Application specific domains @@ -273,3 +274,6 @@ export const NEXT_SYNC_COMMITTEE_INDEX_ELECTRA = 23; export const DEPOSIT_REQUEST_TYPE = 0x00; export const WITHDRAWAL_REQUEST_TYPE = 0x01; export const CONSOLIDATION_REQUEST_TYPE = 0x02; + +// EIP-7805 +export const INCLUSION_LIST_COMMITTEE_SIZE = 16; diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 2c1f0c41da6d..fbcfba3edbf3 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -28,7 +28,7 @@ import { } from "@lodestar/types"; import {LodestarError} from "@lodestar/utils"; import {getTotalSlashingsByIncrement} from "../epoch/processSlashings.js"; -import {AttesterDuty, calculateCommitteeAssignments} from "../util/calculateCommitteeAssignments.js"; +import {AttesterDuty, calculateBeaconCommitteeAssignments} from "../util/calculateBeaconCommitteeAssignments.js"; import { EpochShuffling, IShufflingCache, @@ -760,7 +760,7 @@ export class EpochCache { throw new Error("Attempt to get committees without providing CommitteeIndex"); } - const slotCommittees = this.getShufflingAtSlot(slot).committees[slot % SLOTS_PER_EPOCH]; + const slotCommittees = this.getShufflingAtSlot(slot).beaconCommittees[slot % SLOTS_PER_EPOCH]; const committees = []; for (const index of indices) { @@ -778,7 +778,7 @@ export class EpochCache { } getCommitteeCountPerSlot(epoch: Epoch): number { - return this.getShufflingAtEpoch(epoch).committeesPerSlot; + return this.getShufflingAtEpoch(epoch).beaconCommitteesPerSlot; } /** @@ -917,7 +917,7 @@ export class EpochCache { requestedValidatorIndices: ValidatorIndex[] ): Map { const shuffling = this.getShufflingAtEpoch(epoch); - return calculateCommitteeAssignments(shuffling, requestedValidatorIndices); + return calculateBeaconCommitteeAssignments(shuffling, requestedValidatorIndices); } /** diff --git a/packages/state-transition/src/signatureSets/inclusionList.ts b/packages/state-transition/src/signatureSets/inclusionList.ts new file mode 100644 index 000000000000..7329766896df --- /dev/null +++ b/packages/state-transition/src/signatureSets/inclusionList.ts @@ -0,0 +1,22 @@ +import {DOMAIN_INCLUSION_LIST_COMMITTEE} from "@lodestar/params"; +import {eip7805, ssz} from "@lodestar/types"; + +import {CachedBeaconStateAllForks} from "../types.js"; +import {ISignatureSet, SignatureSetType, computeSigningRoot} from "../util/index.js"; + +export function getInclusionListSignatureSet( + state: CachedBeaconStateAllForks, + inclusionList: eip7805.SignedInclusionList +): ISignatureSet { + const message = inclusionList.message; + const validatorIndex = message.validatorIndex; + const pubkey = state.epochCtx.index2pubkey[validatorIndex]; + const domain = state.config.getDomain(state.slot, DOMAIN_INCLUSION_LIST_COMMITTEE, message.slot); + + return { + type: SignatureSetType.single, + pubkey, + signingRoot: computeSigningRoot(ssz.eip7805.InclusionList, message, domain), + signature: inclusionList.signature, + }; +} diff --git a/packages/state-transition/src/signatureSets/index.ts b/packages/state-transition/src/signatureSets/index.ts index ac10abef6b34..5c9cbb1fdc7a 100644 --- a/packages/state-transition/src/signatureSets/index.ts +++ b/packages/state-transition/src/signatureSets/index.ts @@ -18,6 +18,7 @@ export * from "./proposerSlashings.js"; export * from "./randao.js"; export * from "./voluntaryExits.js"; export * from "./blsToExecutionChange.js"; +export * from "./inclusionList.js"; /** * Includes all signatures on the block (except the deposit signatures) for verification. diff --git a/packages/state-transition/src/util/calculateCommitteeAssignments.ts b/packages/state-transition/src/util/calculateBeaconCommitteeAssignments.ts similarity index 92% rename from packages/state-transition/src/util/calculateCommitteeAssignments.ts rename to packages/state-transition/src/util/calculateBeaconCommitteeAssignments.ts index 008161afa04b..c9a5c6a070ef 100644 --- a/packages/state-transition/src/util/calculateCommitteeAssignments.ts +++ b/packages/state-transition/src/util/calculateBeaconCommitteeAssignments.ts @@ -12,14 +12,14 @@ export interface AttesterDuty { slot: Slot; } -export function calculateCommitteeAssignments( +export function calculateBeaconCommitteeAssignments( epochShuffling: EpochShuffling, requestedValidatorIndices: ValidatorIndex[] ): Map { const requestedValidatorIndicesSet = new Set(requestedValidatorIndices); const duties = new Map(); - const epochCommittees = epochShuffling.committees; + const epochCommittees = epochShuffling.beaconCommittees; for (let epochSlot = 0; epochSlot < SLOTS_PER_EPOCH; epochSlot++) { const slotCommittees = epochCommittees[epochSlot]; for (let i = 0, committeesAtSlot = slotCommittees.length; i < committeesAtSlot; i++) { diff --git a/packages/state-transition/src/util/calculateInclusionListCommitteeAssignments.ts b/packages/state-transition/src/util/calculateInclusionListCommitteeAssignments.ts new file mode 100644 index 000000000000..6026689b9a0c --- /dev/null +++ b/packages/state-transition/src/util/calculateInclusionListCommitteeAssignments.ts @@ -0,0 +1,29 @@ +import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {Root, Slot, ValidatorIndex} from "@lodestar/types"; +import {EpochShuffling} from "./epochShuffling.js"; + +export function calculateInclusionListCommitteeAssignments( + epochShuffling: EpochShuffling, + requestedValidatorIndices: ValidatorIndex[] +): Map { + const requestedValidatorIndicesSet = new Set(requestedValidatorIndices); + const duties = new Map(); + + const epochCommittees = epochShuffling.inclusionListCommittees; + for (let epochSlot = 0; epochSlot < SLOTS_PER_EPOCH; epochSlot++) { + const slotCommittee = epochCommittees[epochSlot]; + + for (let i = 0; i < slotCommittee.length; i++) { + const validatorIndex = slotCommittee[i]; + + if (requestedValidatorIndicesSet.has(validatorIndex)) { + duties.set(validatorIndex, { + slot: epochShuffling.epoch * SLOTS_PER_EPOCH + epochSlot, + committeeRoot: epochShuffling.inclusionListCommitteeRoots[epochSlot], + }); + } + } + } + + return duties; +} diff --git a/packages/state-transition/src/util/epochShuffling.ts b/packages/state-transition/src/util/epochShuffling.ts index 6d0acbd32455..35f1a39c5f64 100644 --- a/packages/state-transition/src/util/epochShuffling.ts +++ b/packages/state-transition/src/util/epochShuffling.ts @@ -2,13 +2,15 @@ import {asyncUnshuffleList, unshuffleList} from "@chainsafe/swap-or-not-shuffle" import {BeaconConfig} from "@lodestar/config"; import { DOMAIN_BEACON_ATTESTER, + DOMAIN_INCLUSION_LIST_COMMITTEE, GENESIS_SLOT, + INCLUSION_LIST_COMMITTEE_SIZE, MAX_COMMITTEES_PER_SLOT, SHUFFLE_ROUND_COUNT, SLOTS_PER_EPOCH, TARGET_COMMITTEE_SIZE, } from "@lodestar/params"; -import {Epoch, RootHex, ValidatorIndex, ssz} from "@lodestar/types"; +import {Epoch, Root, RootHex, ValidatorIndex, ssz} from "@lodestar/types"; import {GaugeExtra, Logger, NoLabels, intDiv, toRootHex} from "@lodestar/utils"; import {BeaconStateAllForks} from "../types.js"; import {getBlockRootAtSlot} from "./blockRoot.js"; @@ -84,28 +86,38 @@ export type EpochShuffling = { /** * List of list of committees Committees * - * Committees by index, by slot + * Beacon committees by index, by slot * * Note: With a high amount of shards, or low amount of validators, * some shards may not have a committee this epoch */ - committees: Uint32Array[][]; + beaconCommittees: Uint32Array[][]; /** - * Committees per slot, for fast attestation verification + * Beacon committees per slot, for fast attestation verification */ - committeesPerSlot: number; + beaconCommitteesPerSlot: number; + + /** + * Inclusion list committees by slot for a epoch + */ + inclusionListCommittees: Uint32Array[]; + + /** + * Inclusion list committee roots by slot for a epoch + */ + inclusionListCommitteeRoots: Root[]; }; -export function computeCommitteeCount(activeValidatorCount: number): number { +export function computeBeaconCommitteeCount(activeValidatorCount: number): number { const validatorsPerSlot = intDiv(activeValidatorCount, SLOTS_PER_EPOCH); const committeesPerSlot = intDiv(validatorsPerSlot, TARGET_COMMITTEE_SIZE); return Math.max(1, Math.min(MAX_COMMITTEES_PER_SLOT, committeesPerSlot)); } -function buildCommitteesFromShuffling(shuffling: Uint32Array): Uint32Array[][] { +function buildBeaconCommitteesFromShuffling(shuffling: Uint32Array): Uint32Array[][] { const activeValidatorCount = shuffling.length; - const committeesPerSlot = computeCommitteeCount(activeValidatorCount); + const committeesPerSlot = computeBeaconCommitteeCount(activeValidatorCount); const committeeCount = committeesPerSlot * SLOTS_PER_EPOCH; const committees = new Array(SLOTS_PER_EPOCH); @@ -128,21 +140,51 @@ function buildCommitteesFromShuffling(shuffling: Uint32Array): Uint32Array[][] { return committees; } +function buildInclusionListCommitteeFromShuffling(shuffling: Uint32Array): { + committees: Uint32Array[]; + committeeRoots: Root[]; +} { + const committees: Uint32Array[] = []; + const committeeRoots: Root[] = []; + + for (let slot = 0; slot < SLOTS_PER_EPOCH; slot++) { + const startOffSet = slot * INCLUSION_LIST_COMMITTEE_SIZE; + const endOffset = startOffSet + INCLUSION_LIST_COMMITTEE_SIZE; + + const slotCommittee = shuffling.subarray(startOffSet, endOffset); + const committeeRoot = ssz.eip7805.InclusionListCommittee.hashTreeRoot([...slotCommittee]); + committees.push(slotCommittee); + committeeRoots.push(committeeRoot); + } + + return {committees, committeeRoots}; +} + export function computeEpochShuffling( // TODO: (@matthewkeil) remove state/epoch and pass in seed to clean this up state: BeaconStateAllForks, activeIndices: Uint32Array, epoch: Epoch ): EpochShuffling { + // Beacon Committee const seed = getSeed(state, epoch, DOMAIN_BEACON_ATTESTER); const shuffling = unshuffleList(activeIndices, seed, SHUFFLE_ROUND_COUNT); - const committees = buildCommitteesFromShuffling(shuffling); + const committees = buildBeaconCommitteesFromShuffling(shuffling); + + // Inclusion List Committee + const ilSeed = getSeed(state, epoch, DOMAIN_INCLUSION_LIST_COMMITTEE); + const ilShuffling = unshuffleList(activeIndices, ilSeed, SHUFFLE_ROUND_COUNT); + const {committees: ilCommittees, committeeRoots: ilCommitteeRoots} = + buildInclusionListCommitteeFromShuffling(ilShuffling); + return { epoch, activeIndices, shuffling, - committees, - committeesPerSlot: committees[0].length, + beaconCommittees: committees, + beaconCommitteesPerSlot: committees[0].length, + inclusionListCommittees: ilCommittees, + inclusionListCommitteeRoots: ilCommitteeRoots, }; } @@ -152,15 +194,25 @@ export async function computeEpochShufflingAsync( activeIndices: Uint32Array, epoch: Epoch ): Promise { + // Beacon Committee const seed = getSeed(state, epoch, DOMAIN_BEACON_ATTESTER); const shuffling = await asyncUnshuffleList(activeIndices, seed, SHUFFLE_ROUND_COUNT); - const committees = buildCommitteesFromShuffling(shuffling); + const committees = buildBeaconCommitteesFromShuffling(shuffling); + + // Inclusion List Committee + const ilSeed = getSeed(state, epoch, DOMAIN_INCLUSION_LIST_COMMITTEE); + const ilShuffling = await asyncUnshuffleList(activeIndices, ilSeed, SHUFFLE_ROUND_COUNT); + const {committees: ilCommittees, committeeRoots: ilCommitteeRoots} = + buildInclusionListCommitteeFromShuffling(ilShuffling); + return { epoch, activeIndices, shuffling, - committees, - committeesPerSlot: committees[0].length, + beaconCommittees: committees, + beaconCommitteesPerSlot: committees[0].length, + inclusionListCommittees: ilCommittees, + inclusionListCommitteeRoots: ilCommitteeRoots, }; } diff --git a/packages/state-transition/src/util/index.ts b/packages/state-transition/src/util/index.ts index b88a20719b85..e08ab434c715 100644 --- a/packages/state-transition/src/util/index.ts +++ b/packages/state-transition/src/util/index.ts @@ -4,7 +4,8 @@ export * from "./attestation.js"; export * from "./attesterStatus.js"; export * from "./balance.js"; export * from "./blindedBlock.js"; -export * from "./calculateCommitteeAssignments.js"; +export * from "./calculateBeaconCommitteeAssignments.js"; +export * from "./calculateInclusionListCommitteeAssignments.js"; export * from "./capella.js"; export * from "./computeAnchorCheckpoint.js"; export * from "./execution.js"; diff --git a/packages/state-transition/test/perf/block/processAttestation.test.ts b/packages/state-transition/test/perf/block/processAttestation.test.ts index 25528b2458e4..4cae336fdd20 100644 --- a/packages/state-transition/test/perf/block/processAttestation.test.ts +++ b/packages/state-transition/test/perf/block/processAttestation.test.ts @@ -120,7 +120,7 @@ describe("altair processAttestation - CachedEpochParticipation.setStatus", () => ); // just get committees of slot 10 let count = 0; - for (const committees of state.epochCtx.currentShuffling.committees[10]) { + for (const committees of state.epochCtx.currentShuffling.beaconCommittees[10]) { for (const committee of committees) { currentEpochParticipation.set(committee, 0b111); count++; diff --git a/packages/state-transition/test/perf/util.ts b/packages/state-transition/test/perf/util.ts index 7dc5daeff754..6a272bb7a5b0 100644 --- a/packages/state-transition/test/perf/util.ts +++ b/packages/state-transition/test/perf/util.ts @@ -16,7 +16,7 @@ import { import {BeaconState, Slot, phase0, ssz} from "@lodestar/types"; import {getEffectiveBalanceIncrements} from "../../src/cache/effectiveBalanceIncrements.js"; import { - computeCommitteeCount, + computeBeaconCommitteeCount, computeEpochAtSlot, createCachedBeaconState, getActiveValidatorIndices, @@ -136,13 +136,13 @@ export function generatePerfTestCachedStatePhase0(opts?: {goBackOneSlot: boolean // previous epoch attestations const numPrevAttestations = SLOTS_PER_EPOCH * MAX_ATTESTATIONS; const activeValidatorCount = pubkeys.length; - const committeesPerSlot = computeCommitteeCount(activeValidatorCount); + const committeesPerSlot = computeBeaconCommitteeCount(activeValidatorCount); for (let i = 0; i < numPrevAttestations; i++) { const slotInEpoch = i % SLOTS_PER_EPOCH; const slot = previousEpoch * SLOTS_PER_EPOCH + slotInEpoch; const index = i % committeesPerSlot; const shuffling = phase0CachedState23637.epochCtx.getShufflingAtEpoch(previousEpoch); - const committee = shuffling.committees[slotInEpoch][index]; + const committee = shuffling.beaconCommittees[slotInEpoch][index]; phase0CachedState23637.previousEpochAttestations.push( ssz.phase0.PendingAttestation.toViewDU({ aggregationBits: BitArray.fromBoolArray(Array.from({length: committee.length}, () => true)), @@ -166,7 +166,7 @@ export function generatePerfTestCachedStatePhase0(opts?: {goBackOneSlot: boolean const slot = currentEpoch * SLOTS_PER_EPOCH + slotInEpoch; const index = i % committeesPerSlot; const shuffling = phase0CachedState23637.epochCtx.getShufflingAtEpoch(previousEpoch); - const committee = shuffling.committees[slotInEpoch][index]; + const committee = shuffling.beaconCommittees[slotInEpoch][index]; phase0CachedState23637.currentEpochAttestations.push( ssz.phase0.PendingAttestation.toViewDU({ diff --git a/packages/state-transition/test/unit/upgradeState.test.ts b/packages/state-transition/test/unit/upgradeState.test.ts index 9923463b46be..0d6825bb0889 100644 --- a/packages/state-transition/test/unit/upgradeState.test.ts +++ b/packages/state-transition/test/unit/upgradeState.test.ts @@ -78,5 +78,14 @@ function getConfig(fork: ForkName, forkEpoch = 0): ChainForkConfig { DENEB_FORK_EPOCH: 0, ELECTRA_FORK_EPOCH: forkEpoch, }); + case ForkName.eip7805: + return createChainForkConfig({ + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: 0, + EIP7805_FORK_EPOCH: forkEpoch, + }); } } diff --git a/packages/types/src/eip7805/index.ts b/packages/types/src/eip7805/index.ts new file mode 100644 index 000000000000..31c28a0d3690 --- /dev/null +++ b/packages/types/src/eip7805/index.ts @@ -0,0 +1,4 @@ +export * from "./types.js"; +import * as ssz from "./sszTypes.js"; +import * as ts from "./types.js"; +export {ts, ssz}; diff --git a/packages/types/src/eip7805/sszTypes.ts b/packages/types/src/eip7805/sszTypes.ts new file mode 100644 index 000000000000..d8a99baea9fb --- /dev/null +++ b/packages/types/src/eip7805/sszTypes.ts @@ -0,0 +1,100 @@ +import {BitVectorType, ContainerType, VectorBasicType} from "@chainsafe/ssz"; +import {INCLUSION_LIST_COMMITTEE_SIZE} from "@lodestar/params"; +import {ssz as bellatrixSsz} from "../bellatrix/index.js"; +import {ssz as electraSsz} from "../electra/index.js"; +import {ssz as primitiveSsz} from "../primitive/index.js"; + +const {Slot, Root, BLSSignature, ValidatorIndex} = primitiveSsz; + +export const InclusionListCommittee = new VectorBasicType(ValidatorIndex, INCLUSION_LIST_COMMITTEE_SIZE); + +export const InclusionList = new ContainerType( + { + slot: Slot, + validatorIndex: ValidatorIndex, + inclusionListCommitteeRoot: Root, + // TODO EIP-7805: the list limit is unreasonable high, on gossip we will reject ILs over 8 Kib but we still + // deserialize before that and load them into memory, hence a sane list limit would be good + transactions: bellatrixSsz.Transactions, + }, + {typeName: "InclusionList", jsonCase: "eth2"} +); + +export const SignedInclusionList = new ContainerType( + { + message: InclusionList, + signature: BLSSignature, + }, + {typeName: "SignedInclusionList", jsonCase: "eth2"} +); + +export const InclusionListByCommitteeIndicesRequest = new ContainerType( + { + slot: Slot, + committeeIndices: new BitVectorType(INCLUSION_LIST_COMMITTEE_SIZE), + }, + {typeName: "InclusionListByCommitteeIndicesRequest", jsonCase: "eth2"} +); + +export const BeaconState = new ContainerType( + { + ...electraSsz.BeaconState.fields, + }, + {typeName: "BeaconState", jsonCase: "eth2"} +); + +export const BeaconBlockBody = new ContainerType( + { + ...electraSsz.BeaconBlockBody.fields, + }, + {typeName: "BeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} +); + +export const BeaconBlock = new ContainerType( + { + ...electraSsz.BeaconBlock.fields, + }, + {typeName: "BeaconBlock", jsonCase: "eth2", cachePermanentRootStruct: true} +); + +export const SignedBeaconBlock = new ContainerType( + { + ...electraSsz.SignedBeaconBlock.fields, + }, + {typeName: "SignedBeaconBlock", jsonCase: "eth2"} +); + +export const BlindedBeaconBlockBody = new ContainerType( + { + ...electraSsz.BlindedBeaconBlockBody.fields, + }, + {typeName: "BlindedBeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} +); + +export const BlindedBeaconBlock = new ContainerType( + { + ...electraSsz.BlindedBeaconBlock.fields, + }, + {typeName: "BlindedBeaconBlock", jsonCase: "eth2", cachePermanentRootStruct: true} +); + +export const SignedBlindedBeaconBlock = new ContainerType( + { + ...electraSsz.SignedBlindedBeaconBlock.fields, + }, + {typeName: "SignedBlindedBeaconBlock", jsonCase: "eth2"} +); + +export const ExecutionPayload = new ContainerType( + { + ...electraSsz.ExecutionPayload.fields, + }, + {typeName: "ExecutionPayload", jsonCase: "eth2"} +); + +export const ExecutionPayloadHeader = new ContainerType( + { + ...electraSsz.ExecutionPayloadHeader.fields, + }, + {typeName: "ExecutionPayloadHeader", jsonCase: "eth2"} +); diff --git a/packages/types/src/eip7805/types.ts b/packages/types/src/eip7805/types.ts new file mode 100644 index 000000000000..abc2c3715362 --- /dev/null +++ b/packages/types/src/eip7805/types.ts @@ -0,0 +1,17 @@ +import {ValueOf} from "@chainsafe/ssz"; +import * as ssz from "./sszTypes.js"; + +export type InclusionListCommittee = ValueOf; +export type InclusionList = ValueOf; +export type SignedInclusionList = ValueOf; +export type InclusionListByCommitteeIndicesRequest = ValueOf; + +export type BeaconState = ValueOf; +export type BeaconBlockBody = ValueOf; +export type BeaconBlock = ValueOf; +export type SignedBeaconBlock = ValueOf; +export type BlindedBeaconBlockBody = ValueOf; +export type BlindedBeaconBlock = ValueOf; +export type SignedBlindedBeaconBlock = ValueOf; +export type ExecutionPayload = ValueOf; +export type ExecutionPayloadHeader = ValueOf; diff --git a/packages/types/src/sszTypes.ts b/packages/types/src/sszTypes.ts index 2a8666f3e243..5bf2b16baec7 100644 --- a/packages/types/src/sszTypes.ts +++ b/packages/types/src/sszTypes.ts @@ -4,11 +4,12 @@ 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 eip7805} from "./eip7805/index.js"; import {ssz as electra} from "./electra/index.js"; import {ssz as phase0} from "./phase0/index.js"; export * from "./primitive/sszTypes.js"; -export {phase0, altair, bellatrix, capella, deneb, electra}; +export {phase0, altair, bellatrix, capella, deneb, electra, eip7805}; /** * Index the ssz types that differ by fork @@ -21,6 +22,7 @@ const typesByFork = { [ForkName.capella]: {...phase0, ...altair, ...bellatrix, ...capella}, [ForkName.deneb]: {...phase0, ...altair, ...bellatrix, ...capella, ...deneb}, [ForkName.electra]: {...phase0, ...altair, ...bellatrix, ...capella, ...deneb, ...electra}, + [ForkName.eip7805]: {...phase0, ...altair, ...bellatrix, ...capella, ...deneb, ...electra, ...eip7805}, }; /** diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index ce54c2f7ae63..479b69b7f817 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -11,6 +11,7 @@ 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 eip7805} from "./eip7805/index.js"; import {ts as electra} from "./electra/index.js"; import {ts as phase0} from "./phase0/index.js"; import {Slot} from "./primitive/types.js"; @@ -22,6 +23,7 @@ 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 eip7805} from "./eip7805/index.js"; /** Common non-spec type to represent roots as strings */ export type RootHex = string; @@ -227,6 +229,44 @@ type TypesByFork = { SignedAggregateAndProof: electra.SignedAggregateAndProof; ExecutionRequests: electra.ExecutionRequests; }; + [ForkName.eip7805]: { + BeaconBlockHeader: phase0.BeaconBlockHeader; + SignedBeaconBlockHeader: phase0.SignedBeaconBlockHeader; + BeaconBlock: electra.BeaconBlock; + BeaconBlockBody: electra.BeaconBlockBody; + BeaconState: electra.BeaconState; + SignedBeaconBlock: electra.SignedBeaconBlock; + Metadata: altair.Metadata; + LightClientHeader: deneb.LightClientHeader; + LightClientBootstrap: electra.LightClientBootstrap; + LightClientUpdate: electra.LightClientUpdate; + LightClientFinalityUpdate: electra.LightClientFinalityUpdate; + LightClientOptimisticUpdate: electra.LightClientOptimisticUpdate; + LightClientStore: electra.LightClientStore; + BlindedBeaconBlock: electra.BlindedBeaconBlock; + BlindedBeaconBlockBody: electra.BlindedBeaconBlockBody; + SignedBlindedBeaconBlock: electra.SignedBlindedBeaconBlock; + ExecutionPayload: deneb.ExecutionPayload; + ExecutionPayloadHeader: deneb.ExecutionPayloadHeader; + BuilderBid: electra.BuilderBid; + SignedBuilderBid: electra.SignedBuilderBid; + SSEPayloadAttributes: electra.SSEPayloadAttributes; + BlockContents: electra.BlockContents; + SignedBlockContents: electra.SignedBlockContents; + ExecutionPayloadAndBlobsBundle: deneb.ExecutionPayloadAndBlobsBundle; + BlobsBundle: deneb.BlobsBundle; + Contents: deneb.Contents; + SyncCommittee: altair.SyncCommittee; + SyncAggregate: altair.SyncAggregate; + SingleAttestation: electra.SingleAttestation; + Attestation: electra.Attestation; + IndexedAttestation: electra.IndexedAttestation; + IndexedAttestationBigint: electra.IndexedAttestationBigint; + AttesterSlashing: electra.AttesterSlashing; + AggregateAndProof: electra.AggregateAndProof; + SignedAggregateAndProof: electra.SignedAggregateAndProof; + ExecutionRequests: electra.ExecutionRequests; + }; }; export type TypesFor = K extends void diff --git a/packages/validator/src/services/inclusionList.ts b/packages/validator/src/services/inclusionList.ts new file mode 100644 index 000000000000..80670528b833 --- /dev/null +++ b/packages/validator/src/services/inclusionList.ts @@ -0,0 +1,108 @@ +import {ApiClient} from "@lodestar/api"; +import {InclusionListDutyList} from "@lodestar/api/lib/beacon/routes/validator.js"; +import {ChainForkConfig} from "@lodestar/config"; +import {Slot, bellatrix, eip7805} from "@lodestar/types"; +import {sleep} from "@lodestar/utils"; +import {IClock, LoggerVc} from "../util/index.js"; +import {ChainHeaderTracker} from "./chainHeaderTracker.js"; +import {ValidatorEventEmitter} from "./emitter.js"; +import {InclusionListDutiesService} from "./inclusionListDuties.js"; +import {SyncingStatusTracker} from "./syncingStatusTracker.js"; +import {ValidatorStore} from "./validatorStore.js"; + +/** + * Service that sets up and handles validator inclusion list duties. + */ +export class InclusionListService { + private readonly dutiesService: InclusionListDutiesService; + + constructor( + private readonly config: ChainForkConfig, + private readonly logger: LoggerVc, + private readonly api: ApiClient, + private readonly clock: IClock, + private readonly validatorStore: ValidatorStore, + private readonly emitter: ValidatorEventEmitter, + chainHeadTracker: ChainHeaderTracker, + syncingStatusTracker: SyncingStatusTracker + ) { + this.dutiesService = new InclusionListDutiesService( + config, + logger, + api, + clock, + validatorStore, + chainHeadTracker, + syncingStatusTracker + ); + + // At most every slot, check existing duties from InclusionListDutiesService and run tasks + clock.runEverySlot(this.runInclusionListTasks); + } + + private runInclusionListTasks = async (slot: Slot, signal: AbortSignal): Promise => { + // Fetch info first so a potential delay is absorbed by the sleep() below + const duties = this.dutiesService.getDutiesAtSlot(slot); + if (duties.length === 0) { + return; + } + + // A validator should create and broadcast the IL when either + // (a) the validator has received a valid block from the expected block proposer for the assigned slot or + // (b) one-third of the slot has transpired (SECONDS_PER_SLOT / 3 seconds after the start of slot) -- whichever comes first. + // TODO EIP-7805: Review this timing. Spec says only mandates us to broadcast before 11s + await Promise.race([sleep(this.clock.msToSlot(slot + 1 / 3), signal), this.emitter.waitForBlockSlot(slot)]); + + // If there is more than one duty, all validators on duty will sign and publish the same IL + const inclusionListTransactions = await this.produceInclusionList(slot); + + await this.signAndPublishInclusionList(inclusionListTransactions, duties); + }; + + // Note: The inclusion list returned here is a "blueprint" ie. every field + // is filled except validator index = 0. Need to replace validator index to + // form a valid InclusionList + private async produceInclusionList(slot: Slot): Promise { + // Produce one IL per slot + return (await this.api.validator.produceInclusionList({slot})).value(); + } + + /** + * Only one `InclusionList` is downloaded from the BN. It is then signed by each + * validator and the list of individually-signed `InclusionList` objects is returned to the BN. + */ + + private async signAndPublishInclusionList( + inclusionListTransactions: bellatrix.Transactions, + duties: InclusionListDutyList + ) { + const signedInclusionLists: eip7805.SignedInclusionList[] = []; + + await Promise.all( + duties.map(async (duty) => { + const inclusionList: eip7805.InclusionList = { + slot: duty.slot, + validatorIndex: duty.validatorIndex, + inclusionListCommitteeRoot: duty.inclusionListCommitteeRoot, + transactions: inclusionListTransactions, + }; + // TODO EIP-7805: Log and log context here + try { + signedInclusionLists.push(await this.validatorStore.signInclusionList(duty, inclusionList)); + } catch (e) { + this.logger.error("Error signing inclusion list", {}, e as Error); + } + }) + ); + + // Publish ILs right away + for (const signedInclusionList of signedInclusionLists) { + try { + (await this.api.validator.publishInclusionList({signedInclusionList})).assertOk(); + this.logger.info(`Published inclusionList ${signedInclusionList.message}`); + } catch (e) { + this.logger.error("Error publishing inclusionList", {}, e as Error); + } + } + } +} diff --git a/packages/validator/src/services/inclusionListDuties.ts b/packages/validator/src/services/inclusionListDuties.ts new file mode 100644 index 000000000000..d0d35952a202 --- /dev/null +++ b/packages/validator/src/services/inclusionListDuties.ts @@ -0,0 +1,302 @@ +import {ApiClient, routes} from "@lodestar/api"; +import {ChainForkConfig} from "@lodestar/config"; +import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {computeEpochAtSlot, isAggregatorFromCommitteeLength, isStartSlotOfEpoch} from "@lodestar/state-transition"; +import {Epoch, RootHex, Slot, ValidatorIndex} from "@lodestar/types"; +import {sleep, toPubkeyHex} from "@lodestar/utils"; +import {IClock, LoggerVc} from "../util/index.js"; +import {ChainHeaderTracker, HeadEventData} from "./chainHeaderTracker.js"; +import {SyncingStatusTracker} from "./syncingStatusTracker.js"; +import {ValidatorStore} from "./validatorStore.js"; + +/** Only retain `HISTORICAL_DUTIES_EPOCHS` duties prior to the current epoch. */ +// TODO EIP-7805: Do we need 2 epochs like attestations? +const HISTORICAL_DUTIES_EPOCHS = 2; + +const EIP7805_FORK_LOOKAHEAD_EPOCHS = 1; + +type InclusionListDuty = routes.validator.InclusionListDuty; +// To assist with readability +type InclusionListDutiesAtEpoch = {dependentRoot: RootHex; dutiesByIndex: Map}; + +/** + * Similar to AttestationDutiesService but + * - No generating and caching selection proof + * - No handling and maintaining subnet subscription + */ +export class InclusionListDutiesService { + /** Maps a validator public key to their duties for each epoch */ + private readonly dutiesByIndexByEpoch = new Map(); + /** + * We may receive new dependentRoot of an epoch but it's not the last slot of epoch + * so we have to wait for getting close to the next epoch to redownload new inclusionListDuties. + */ + private readonly pendingDependentRootByEpoch = new Map(); + + constructor( + private readonly config: ChainForkConfig, + private readonly logger: LoggerVc, + private readonly api: ApiClient, + private clock: IClock, + private readonly validatorStore: ValidatorStore, + chainHeadTracker: ChainHeaderTracker, + syncingStatusTracker: SyncingStatusTracker + ) { + // Running this task every epoch is safe since a re-org of two epochs is very unlikely + // TODO: If the re-org event is reliable consider re-running then + clock.runEveryEpoch(this.runDutiesTasks); + clock.runEverySlot(this.prepareForNextEpoch); + chainHeadTracker.runOnNewHead(this.onNewHead); + syncingStatusTracker.runOnResynced(async (slot) => { + // Skip on first slot of epoch since tasks are already scheduled + if (!isStartSlotOfEpoch(slot)) { + return this.runDutiesTasks(computeEpochAtSlot(slot)); + } + }); + } + + /** Returns all `ValidatorDuty` for the given `slot` */ + getDutiesAtSlot(slot: Slot): InclusionListDuty[] { + const epoch = computeEpochAtSlot(slot); + const duties: InclusionListDuty[] = []; + const epochDuties = this.dutiesByIndexByEpoch.get(epoch); + if (epochDuties === undefined) { + return duties; + } + + for (const validatorDuty of epochDuties.dutiesByIndex.values()) { + if (validatorDuty.slot === slot) { + duties.push(validatorDuty); + } + } + + return duties; + } + + /** + * If a reorg dependent root comes at a slot other than last slot of epoch + * just update this.pendingDependentRootByEpoch() and process here + */ + private prepareForNextEpoch = async (slot: Slot, signal: AbortSignal): Promise => { + // only interested in last slot of epoch + if ((slot + 1) % SLOTS_PER_EPOCH !== 0) { + return; + } + + // during the 1 / 3 of epoch, last block of epoch may come + await sleep(this.clock.msToSlot(slot + 1 / 3), signal); + + const nextEpoch = computeEpochAtSlot(slot) + 1; + const dependentRoot = this.dutiesByIndexByEpoch.get(nextEpoch)?.dependentRoot; + const pendingDependentRoot = this.pendingDependentRootByEpoch.get(nextEpoch); + if (dependentRoot && pendingDependentRoot && dependentRoot !== pendingDependentRoot) { + // this happens when pendingDependentRoot is not the last block of an epoch + this.logger.info("Redownload inclusion list duties when it's close to epoch boundary", {nextEpoch, slot}); + await this.handleInclusionListDutiesReorg(nextEpoch, slot, dependentRoot, pendingDependentRoot); + } + }; + + private runDutiesTasks = async (epoch: Epoch): Promise => { + // Before EIP-7805 fork (+ lookahead) no need to check duties + if (epoch < this.config.EIP7805_FORK_EPOCH - EIP7805_FORK_LOOKAHEAD_EPOCHS) { + return; + } + + await Promise.all([ + // Run pollInclusionListCommittee immediately for all known local indices + this.pollInclusionListCommittee(epoch, this.validatorStore.getAllLocalIndices()).catch((e: Error) => { + this.logger.error("Error on poll inclusion list committee", {epoch}, e); + }), + + // At the same time fetch any remaining unknown validator indices, then poll duties for those newIndices only + this.validatorStore + .pollValidatorIndices() + .then((newIndices) => this.pollInclusionListCommittee(epoch, newIndices)) + .catch((e: Error) => { + this.logger.error("Error on poll indices and inclusion list committee", {epoch}, e); + }), + ]); + + // After both, prune + this.pruneOldDuties(epoch); + }; + + /** + * Query the beacon node for inclusion list duties for any known validators. + * + * This function will perform (in the following order): + * + * 1. Poll for current-epoch duties and update the local duties map. + * 2. As above, but for the next-epoch. + * 3. Prune old entries from duties. + */ + private async pollInclusionListCommittee(currentEpoch: Epoch, indexArr: ValidatorIndex[]): Promise { + const nextEpoch = currentEpoch + 1; + + // No need to bother the BN if we don't have any validators. + if (indexArr.length === 0) { + return; + } + + for (const epoch of [currentEpoch, nextEpoch]) { + // Download the duties and update the duties for the current and next epoch. + await this.pollInclusionListCommitteeForEpoch(epoch, indexArr).catch((e: Error) => { + this.logger.error("Failed to download inclusion list duties", {epoch}, e); + }); + } + } + + /** + * For the given `indexArr`, download the duties for the given `epoch` and store them in duties. + */ + private async pollInclusionListCommitteeForEpoch(epoch: Epoch, indexArr: ValidatorIndex[]): Promise { + // Don't fetch duties for epochs before genesis. However, should fetch epoch 0 duties at epoch -1 + if (epoch < 0) { + return; + } + + const res = await this.api.validator.getInclusionListCommitteeDuties({epoch, indices: indexArr}); + const inclusionListDuties = res.value(); + const {dependentRoot} = res.meta(); + const relevantDuties = inclusionListDuties.filter((duty) => { + const pubkeyHex = toPubkeyHex(duty.pubkey); + return this.validatorStore.hasVotingPubkey(pubkeyHex) && this.validatorStore.isDoppelgangerSafe(pubkeyHex); + }); + + this.logger.debug("Downloaded inclusion list duties", {epoch, dependentRoot, count: relevantDuties.length}); + + const dutiesAtEpoch = this.dutiesByIndexByEpoch.get(epoch); + const priorDependentRoot = dutiesAtEpoch?.dependentRoot; + const dependentRootChanged = priorDependentRoot !== undefined && priorDependentRoot !== dependentRoot; + + if (!priorDependentRoot || dependentRootChanged) { + const dutiesByIndex = new Map(); + for (const duty of relevantDuties) { + dutiesByIndex.set(duty.validatorIndex, duty); + } + this.dutiesByIndexByEpoch.set(epoch, {dependentRoot, dutiesByIndex}); + + if (priorDependentRoot && dependentRootChanged) { + this.logger.warn("Inclusion list duties re-org. This may happen from time to time", { + priorDependentRoot: priorDependentRoot, + dependentRoot: dependentRoot, + epoch, + }); + } + } else { + const existingDuties = dutiesAtEpoch.dutiesByIndex; + const existingDutiesCount = existingDuties.size; + const discoveredNewDuties = relevantDuties.length > existingDutiesCount; + + if (discoveredNewDuties) { + for (const duty of relevantDuties) { + if (!existingDuties.has(duty.validatorIndex)) { + existingDuties.set(duty.validatorIndex, duty); + } + } + + this.logger.debug("Discovered new inclusion list duties", { + epoch, + dependentRoot, + count: relevantDuties.length - existingDutiesCount, + }); + } + } + } + + private async handleInclusionListDutiesReorg( + dutyEpoch: Epoch, + slot: Slot, + oldDependentRoot: RootHex, + newDependentRoot: RootHex + ): Promise { + const logContext = {dutyEpoch, slot, oldDependentRoot, newDependentRoot}; + this.logger.debug("Redownload inclusion list duties", logContext); + + await this.pollInclusionListCommitteeForEpoch(dutyEpoch, this.validatorStore.getAllLocalIndices()) + .then(() => { + this.pendingDependentRootByEpoch.delete(dutyEpoch); + }) + .catch((e: Error) => { + this.logger.error("Failed to redownload inclusion list duties when reorg happens", logContext, e); + }); + } + + /** + * inclusion list duties may be reorged due to 2 scenarios: + * 1. node is syncing (for nextEpoch duties) + * 2. node is reorged + * previousDutyDependentRoot = get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1) + * => dependent root of current epoch + * currentDutyDependentRoot = get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch) - 1) + * => dependent root of next epoch + */ + private onNewHead = async ({ + slot, + head, + previousDutyDependentRoot, + currentDutyDependentRoot, + }: HeadEventData): Promise => { + const currentEpoch = computeEpochAtSlot(slot); + const nextEpoch = currentEpoch + 1; + const nextTwoEpoch = currentEpoch + 2; + const nextTwoEpochDependentRoot = this.dutiesByIndexByEpoch.get(currentEpoch + 2)?.dependentRoot; + + // this may happen ONLY when node is syncing + // it's safe to get inclusion list duties at epoch n + 1 thanks to nextEpochShuffling cache + // but it's an issue to request inclusion list duties for epoch n + 2 as dependent root keeps changing while node is syncing + // see https://github.com/ChainSafe/lodestar/issues/3211 + if (nextTwoEpochDependentRoot && head !== nextTwoEpochDependentRoot) { + // last slot of epoch, we're sure it's the correct dependent root + if ((slot + 1) % SLOTS_PER_EPOCH === 0) { + this.logger.info("Next 2 epoch inclusion list duties reorg", {slot, dutyEpoch: nextTwoEpoch, head}); + await this.handleInclusionListDutiesReorg(nextTwoEpoch, slot, nextTwoEpochDependentRoot, head); + } else { + this.logger.debug("Potential next 2 epoch inclusion list duties reorg", {slot, dutyEpoch: nextTwoEpoch, head}); + // node may send adjacent onHead events while it's syncing + // wait for getting close to next epoch to make sure the dependRoot + this.pendingDependentRootByEpoch.set(nextTwoEpoch, head); + } + } + + // dependent root for next epoch changed + const nextEpochDependentRoot = this.dutiesByIndexByEpoch.get(nextEpoch)?.dependentRoot; + if (nextEpochDependentRoot && currentDutyDependentRoot !== nextEpochDependentRoot) { + this.logger.warn("Potential next epoch inclusion list duties reorg", { + slot, + dutyEpoch: nextEpoch, + priorDependentRoot: nextEpochDependentRoot, + newDependentRoot: currentDutyDependentRoot, + }); + await this.handleInclusionListDutiesReorg(nextEpoch, slot, nextEpochDependentRoot, currentDutyDependentRoot); + } + + // dependent root for current epoch changed + const currentEpochDependentRoot = this.dutiesByIndexByEpoch.get(currentEpoch)?.dependentRoot; + if (currentEpochDependentRoot && currentEpochDependentRoot !== previousDutyDependentRoot) { + this.logger.warn("Potential current epoch inclusion list duties reorg", { + slot, + dutyEpoch: currentEpoch, + priorDependentRoot: currentEpochDependentRoot, + newDependentRoot: previousDutyDependentRoot, + }); + await this.handleInclusionListDutiesReorg( + currentEpoch, + slot, + currentEpochDependentRoot, + previousDutyDependentRoot + ); + } + }; + + /** Run once per epoch to prune duties map */ + private pruneOldDuties(currentEpoch: Epoch): void { + for (const byEpochMap of [this.dutiesByIndexByEpoch, this.pendingDependentRootByEpoch]) { + for (const epoch of byEpochMap.keys()) { + if (epoch + HISTORICAL_DUTIES_EPOCHS < currentEpoch) { + byEpochMap.delete(epoch); + } + } + } + } +} diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 565574c758cc..bb577fbb56e6 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -8,6 +8,7 @@ import { DOMAIN_BEACON_ATTESTER, DOMAIN_BEACON_PROPOSER, DOMAIN_CONTRIBUTION_AND_PROOF, + DOMAIN_INCLUSION_LIST_COMMITTEE, DOMAIN_RANDAO, DOMAIN_SELECTION_PROOF, DOMAIN_SYNC_COMMITTEE, @@ -39,6 +40,7 @@ import { ValidatorIndex, altair, bellatrix, + eip7805, phase0, ssz, } from "@lodestar/types"; @@ -689,6 +691,26 @@ export class ValidatorStore { }; } + async signInclusionList( + duty: routes.validator.InclusionListDuty, + inclusionList: eip7805.InclusionList + ): Promise { + this.validateInclusionListDuty(duty, inclusionList); + + const domain = this.config.getDomain(inclusionList.slot, DOMAIN_INCLUSION_LIST_COMMITTEE); + const signingRoot = computeSigningRoot(ssz.eip7805.InclusionList, inclusionList, domain); + + const signableMessage: SignableMessage = { + type: SignableMessageType.INCLUSION_LIST, + data: inclusionList, + }; + + return { + message: inclusionList, + signature: await this.getSignature(duty.pubkey, signingRoot, inclusionList.slot, signableMessage), + }; + } + isDoppelgangerSafe(pubkeyHex: PubkeyHex): boolean { // If doppelganger is not enabled we assumed all keys to be safe for use return !this.doppelgangerService || this.doppelgangerService.isDoppelgangerSafe(pubkeyHex); @@ -818,6 +840,18 @@ export class ValidatorStore { } } + /** Prevent signing bad data sent by the Beacon node */ + private validateInclusionListDuty( + duty: routes.validator.InclusionListDuty, + inclusionList: eip7805.InclusionList + ): void { + if (duty.slot !== inclusionList.slot) { + throw Error(`Inconsistent duties during signing: duty.slot ${duty.slot} != il.slot ${inclusionList.slot}`); + } + + // TODO EIP-7805: Maybe check if validator index in inclusionListCommitteeRoot? + } + private assertDoppelgangerSafe(pubKey: PubkeyHex | BLSPubkey): void { const pubkeyHex = typeof pubKey === "string" ? pubKey : toPubkeyHex(pubKey); if (!this.isDoppelgangerSafe(pubkeyHex)) { diff --git a/packages/validator/src/util/externalSignerClient.ts b/packages/validator/src/util/externalSignerClient.ts index ab5849cf1a6b..fde1839038db 100644 --- a/packages/validator/src/util/externalSignerClient.ts +++ b/packages/validator/src/util/externalSignerClient.ts @@ -13,6 +13,7 @@ import { Slot, altair, capella, + eip7805, phase0, ssz, sszTypesFor, @@ -35,6 +36,7 @@ export enum SignableMessageType { SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF = "SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF", VALIDATOR_REGISTRATION = "VALIDATOR_REGISTRATION", BLS_TO_EXECUTION_CHANGE = "BLS_TO_EXECUTION_CHANGE", + INCLUSION_LIST = "INCLUSION_LIST", } const AggregationSlotType = new ContainerType({ @@ -84,7 +86,8 @@ export type SignableMessage = | {type: SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF; data: ValueOf} | {type: SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF; data: altair.ContributionAndProof} | {type: SignableMessageType.VALIDATOR_REGISTRATION; data: ValidatorRegistrationV1} - | {type: SignableMessageType.BLS_TO_EXECUTION_CHANGE; data: capella.BLSToExecutionChange}; + | {type: SignableMessageType.BLS_TO_EXECUTION_CHANGE; data: capella.BLSToExecutionChange} + | {type: SignableMessageType.INCLUSION_LIST; data: eip7805.InclusionList}; const requiresForkInfo: Record = { [SignableMessageType.AGGREGATION_SLOT]: true, @@ -100,6 +103,7 @@ const requiresForkInfo: Record = { [SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF]: true, [SignableMessageType.VALIDATOR_REGISTRATION]: false, [SignableMessageType.BLS_TO_EXECUTION_CHANGE]: true, + [SignableMessageType.INCLUSION_LIST]: false, }; type Web3SignerSerializedRequest = { @@ -274,5 +278,8 @@ function serializerSignableMessagePayload(config: BeaconConfig, payload: Signabl case SignableMessageType.BLS_TO_EXECUTION_CHANGE: return {BLS_TO_EXECUTION_CHANGE: ssz.capella.BLSToExecutionChange.toJson(payload.data)}; + + case SignableMessageType.INCLUSION_LIST: + return {inclusion_list: ssz.eip7805.InclusionList.toJson(payload.data)}; } } diff --git a/packages/validator/src/util/params.ts b/packages/validator/src/util/params.ts index 383275bfb47b..4a20543b03d6 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -72,6 +72,7 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record