UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

1,229 lines (1,108 loc) 56.1 kB
import {routes} from "@lodestar/api"; import {BeaconConfig, ChainForkConfig} from "@lodestar/config"; import {PayloadStatus} from "@lodestar/fork-choice"; import { ForkName, ForkPostDeneb, ForkPostElectra, ForkPostGloas, ForkPreElectra, ForkSeq, NUMBER_OF_COLUMNS, isForkPostElectra, isForkPostGloas, } from "@lodestar/params"; import {computeTimeAtSlot} from "@lodestar/state-transition"; import { Root, SignedBeaconBlock, SingleAttestation, Slot, SubnetID, UintNum64, deneb, fulu, gloas, isGloasDataColumnSidecar, ssz, sszTypesFor, } from "@lodestar/types"; import {LogLevel, Logger, prettyBytes, toHex, toRootHex} from "@lodestar/utils"; import { BlockInput, BlockInputColumns, BlockInputSource, IBlockInput, isBlockInputColumns, } from "../../chain/blocks/blockInput/index.js"; import {PayloadError, PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js"; import {PayloadEnvelopeInput, PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js"; import {BlobSidecarValidation} from "../../chain/blocks/types.js"; import {ChainEvent} from "../../chain/emitter.js"; import { AttestationError, AttestationErrorCode, BlobSidecarErrorCode, BlobSidecarGossipError, BlockError, BlockErrorCode, BlockGossipError, DataColumnSidecarErrorCode, DataColumnSidecarGossipError, ExecutionPayloadEnvelopeError, ExecutionPayloadEnvelopeErrorCode, GossipAction, GossipActionError, PayloadAttestationError, PayloadAttestationErrorCode, SyncCommitteeError, } from "../../chain/errors/index.js"; import {IBeaconChain} from "../../chain/interface.js"; import {validateGossipBlobSidecar} from "../../chain/validation/blobSidecar.js"; import { validateGossipFuluDataColumnSidecar, validateGossipGloasDataColumnSidecar, } from "../../chain/validation/dataColumnSidecar.js"; import {validateGossipExecutionPayloadBid} from "../../chain/validation/executionPayloadBid.js"; import {validateGossipExecutionPayloadEnvelope} from "../../chain/validation/executionPayloadEnvelope.js"; import { AggregateAndProofValidationResult, GossipAttestation, toElectraSingleAttestation, validateGossipAggregateAndProof, validateGossipAttestationsSameAttData, validateGossipAttesterSlashing, validateGossipBlock, validateGossipBlsToExecutionChange, validateGossipProposerSlashing, validateGossipSyncCommittee, validateGossipVoluntaryExit, validateSyncCommitteeGossipContributionAndProof, } from "../../chain/validation/index.js"; import {validateLightClientFinalityUpdate} from "../../chain/validation/lightClientFinalityUpdate.js"; import {validateLightClientOptimisticUpdate} from "../../chain/validation/lightClientOptimisticUpdate.js"; import {validateGossipPayloadAttestationMessage} from "../../chain/validation/payloadAttestationMessage.js"; import {validateGossipProposerPreferences} from "../../chain/validation/proposerPreferences.js"; import {OpSource} from "../../chain/validatorMonitor.js"; import {Metrics} from "../../metrics/index.js"; import {kzgCommitmentToVersionedHash} from "../../util/blobs.js"; import {getBlobKzgCommitments, getDataColumnSidecarSlot} from "../../util/dataColumns.js"; import {INetworkCore} from "../core/index.js"; import {NetworkEventBus} from "../events.js"; import { BatchGossipHandlers, GossipHandlerParamGeneric, GossipHandlers, GossipType, SequentialGossipHandlers, } from "../gossip/interface.js"; import {sszDeserialize} from "../gossip/topic.js"; import {INetwork} from "../interface.js"; import {PeerAction} from "../peers/index.js"; import {AggregatorTracker} from "./aggregatorTracker.js"; /** * Gossip handler options as part of network options */ export type GossipHandlerOpts = { /** By default pass gossip attestations to forkchoice */ dontSendGossipAttestationsToForkchoice?: boolean; }; export type ValidatorFnsModules = { chain: IBeaconChain; config: BeaconConfig; logger: Logger; metrics: Metrics | null; events: NetworkEventBus; aggregatorTracker: AggregatorTracker; core: INetworkCore; }; const MAX_UNKNOWN_BLOCK_ROOT_RETRIES = 1; const BLOCK_AVAILABILITY_CUTOFF_MS = 3_000; /** * Gossip handlers perform validation + handling in a single function. * - This gossip handlers MUST only be registered as validator functions. No handler is registered for any topic. * - All `chain/validation/*` functions MUST throw typed GossipActionError instances so they gossip action is captured * by `getGossipValidatorFn()` try catch block. * - This gossip handlers should not let any handling errors propagate to the caller. Only validation errors must be thrown. * * Note: `libp2p/js-libp2p-interfaces` would normally indicate to register separate validator functions and handler functions. * This approach is not suitable for us because: * - We do expensive processing on the object in the validator function that we need to re-use in the handler function. * - The validator function produces extra data that is needed for the handler function. Making this data available in * the handler function scope is hard to achieve without very hacky strategies * - Ethereum Consensus gossipsub protocol strictly defined a single topic for message */ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipHandlerOpts): GossipHandlers { return {...getSequentialHandlers(modules, options), ...getBatchHandlers(modules, options)}; } /** * Default handlers validate gossip messages one by one. * We only have a choice to do batch validation for beacon_attestation topic. */ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHandlerOpts): SequentialGossipHandlers { const {chain, config, metrics, logger, core} = modules; async function validateBeaconBlock( signedBlock: SignedBeaconBlock, fork: ForkName, peerIdStr: string, seenTimestampSec: number ): Promise<IBlockInput> { const slot = signedBlock.message.slot; const forkTypes = config.getForkTypes(slot); const blockRootHex = toRootHex(forkTypes.BeaconBlock.hashTreeRoot(signedBlock.message)); const blockShortHex = prettyBytes(blockRootHex); const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec); const recvToValLatency = Date.now() / 1000 - seenTimestampSec; // always set block to seen cache for all forks so that we don't need to download it // TODO: validate block before adding to cache // tracked in https://github.com/ChainSafe/lodestar/issues/7957 const logCtx = { currentSlot: chain.clock.currentSlot, peerId: peerIdStr, delaySec, recvToValLatency, }; logger.debug("Received gossip block", {...logCtx}); // optimistically add gossip block to the seen cache // if validation fails, we will NOT forward this gossip block to peers // - if PARENT_UNKNOWN error, blockInput will then be queued inside BlockInputSync. If the gossip block is really invalid, it will be pruned there // - if other validator errors, blockInput will stay in the seen cache and will be pruned on finalization const blockInput = chain.seenBlockInputCache.getByBlock({ block: signedBlock, blockRootHex, source: BlockInputSource.gossip, seenTimestampSec, peerIdStr, }); try { await validateGossipBlock(config, chain, signedBlock, fork); if (isForkPostGloas(fork)) { chain.seenPayloadEnvelopeInputCache.add({ blockRootHex, block: signedBlock as SignedBeaconBlock<ForkPostGloas>, forkName: fork, sampledColumns: chain.custodyConfig.sampledColumns, custodyColumns: chain.custodyConfig.custodyColumns, timeCreatedSec: seenTimestampSec, }); } const blockInputMeta = blockInput.getLogMeta(); const recvToValidation = Date.now() / 1000 - seenTimestampSec; const validationTime = recvToValidation - recvToValLatency; metrics?.gossipBlock.gossipValidation.recvToValidation.observe(recvToValidation); metrics?.gossipBlock.gossipValidation.validationTime.observe(validationTime); logger.debug("Validated gossip block", {...blockInputMeta, ...logCtx, recvToValidation, validationTime}); chain.emitter.emit(routes.events.EventType.blockGossip, {slot, block: blockRootHex}); return blockInput; } catch (e) { if (e instanceof BlockGossipError) { logger.debug("Gossip block has error", {slot, root: blockShortHex, code: e.type.code}); if ( (e.type.code === BlockErrorCode.PARENT_UNKNOWN || e.type.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN) && blockInput ) { chain.emitter.emit(ChainEvent.blockUnknownParent, { blockInput, peer: peerIdStr, source: BlockInputSource.gossip, }); // throw error (don't prune the blockInput) throw e; } if (e.action === GossipAction.REJECT) { chain.persistInvalidSszValue(forkTypes.SignedBeaconBlock, signedBlock, `gossip_reject_slot_${slot}`); } } chain.seenBlockInputCache.prune(blockRootHex); throw e; } } async function validateBeaconBlob( blobSidecar: deneb.BlobSidecar, subnet: SubnetID, peerIdStr: string, seenTimestampSec: number ): Promise<BlockInput> { const blobBlockHeader = blobSidecar.signedBlockHeader.message; const slot = blobBlockHeader.slot; const fork = config.getForkName(slot); const blockRootHex = toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(blobBlockHeader)); const blockShortHex = prettyBytes(blockRootHex); const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec); const recvToValLatency = Date.now() / 1000 - seenTimestampSec; try { await validateGossipBlobSidecar(fork, chain, blobSidecar, subnet); const blockInput = chain.seenBlockInputCache.getByBlob({ blockRootHex, blobSidecar, source: BlockInputSource.gossip, seenTimestampSec, peerIdStr, }); const recvToValidation = Date.now() / 1000 - seenTimestampSec; const validationTime = recvToValidation - recvToValLatency; metrics?.gossipBlob.recvToValidation.observe(recvToValidation); metrics?.gossipBlob.validationTime.observe(validationTime); if (chain.emitter.listenerCount(routes.events.EventType.blobSidecar)) { let versionedHash: Uint8Array; if (blockInput.hasBlock()) { // if block hasn't arrived yet then this will throw and need to calculate the versionedHash as a 1-off versionedHash = blockInput.getVersionedHashes()[blobSidecar.index]; } else { versionedHash = kzgCommitmentToVersionedHash(blobSidecar.kzgCommitment); } chain.emitter.emit(routes.events.EventType.blobSidecar, { blockRoot: blockRootHex, slot, index: blobSidecar.index, kzgCommitment: toHex(blobSidecar.kzgCommitment), versionedHash: toHex(versionedHash), }); } logger.debug("Received gossip blob", { ...blockInput.getLogMeta(), currentSlot: chain.clock.currentSlot, peerId: peerIdStr, delaySec, subnet, recvToValLatency, recvToValidation, validationTime, }); return blockInput; } catch (e) { if (e instanceof BlobSidecarGossipError) { // Don't trigger this yet if full block and blobs haven't arrived yet if (e.type.code === BlobSidecarErrorCode.PARENT_UNKNOWN) { logger.debug("Gossip blob has error", {slot, root: blockShortHex, code: e.type.code}); // no need to trigger `unknownBlockParent` event here, as we already did it in `validateBeaconBlock()` // // TODO(fulu): is this note above correct? Could have random blob that we see that could trigger // unknownBlockSync. And duplicate addition of a block will be deduplicated by the // BlockInputSync event handler. Check this!! // events.emit(NetworkEvent.unknownBlockParent, {blockInput, peer: peerIdStr}); } if (e.action === GossipAction.REJECT) { chain.persistInvalidSszValue( ssz.deneb.BlobSidecar, blobSidecar, `gossip_reject_slot_${slot}_index_${blobSidecar.index}` ); } } throw e; } } async function validateBeaconDataColumn( dataColumnSidecar: fulu.DataColumnSidecar, _dataColumnBytes: Uint8Array, gossipSubnet: SubnetID, peerIdStr: string, seenTimestampSec: number ): Promise<BlockInputColumns> { metrics?.peerDas.dataColumnSidecarProcessingRequests.inc(); const dataColumnBlockHeader = dataColumnSidecar.signedBlockHeader.message; const slot = dataColumnBlockHeader.slot; const blockRootHex = toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(dataColumnBlockHeader)); // check to see if block has already been processed and BlockInput has been deleted (column received via reqresp or other means) if (chain.forkChoice.hasBlockHex(blockRootHex)) { metrics?.peerDas.dataColumnSidecarProcessingSkip.inc(); logger.debug("Already processed block for column sidecar, skipping processing", { slot, blockRoot: blockRootHex, index: dataColumnSidecar.index, }); throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { code: DataColumnSidecarErrorCode.ALREADY_KNOWN, columnIndex: dataColumnSidecar.index, slot, }); } // first check if we should even process this column (we may have already processed it via getBlobsV2) { const blockInput = chain.seenBlockInputCache.get(blockRootHex); if (blockInput && isBlockInputColumns(blockInput) && blockInput.hasColumn(dataColumnSidecar.index)) { metrics?.peerDas.dataColumnSidecarProcessingSkip.inc(); logger.debug("Already have column sidecar in BlockInput, skipping processing", { ...blockInput.getLogMeta(), index: dataColumnSidecar.index, }); throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { code: DataColumnSidecarErrorCode.ALREADY_KNOWN, columnIndex: dataColumnSidecar.index, slot, }); } } const verificationTimer = metrics?.peerDas.dataColumnSidecarGossipVerificationTime.startTimer(); const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec); const secFromSlot = chain.clock.secFromSlot(slot); const recvToValLatency = Date.now() / 1000 - seenTimestampSec; try { await validateGossipFuluDataColumnSidecar(chain, dataColumnSidecar, gossipSubnet, metrics); const blockInput = chain.seenBlockInputCache.getByColumn({ blockRootHex, columnSidecar: dataColumnSidecar, source: BlockInputSource.gossip, seenTimestampSec, peerIdStr, }); const recvToValidation = Date.now() / 1000 - seenTimestampSec; const validationTime = recvToValidation - recvToValLatency; metrics?.peerDas.dataColumnSidecarProcessingSuccesses.inc(); metrics?.gossipBlob.recvToValidation.observe(recvToValidation); metrics?.gossipBlob.validationTime.observe(validationTime); if (chain.emitter.listenerCount(routes.events.EventType.dataColumnSidecar)) { chain.emitter.emit(routes.events.EventType.dataColumnSidecar, { blockRoot: blockRootHex, slot, index: dataColumnSidecar.index, kzgCommitments: dataColumnSidecar.kzgCommitments.map(toHex), }); } logger.debug("Received gossip dataColumn", { ...blockInput.getLogMeta(), currentSlot: chain.clock.currentSlot, peerId: peerIdStr, delaySec, secFromSlot, gossipSubnet, columnIndex: dataColumnSidecar.index, recvToValLatency, recvToValidation, validationTime, }); return blockInput; } catch (e) { if (e instanceof DataColumnSidecarGossipError && e.action === GossipAction.REJECT) { chain.persistInvalidSszValue( ssz.fulu.DataColumnSidecar, dataColumnSidecar, `gossip_reject_slot_${slot}_index_${dataColumnSidecar.index}` ); // no need to trigger `unknownBlockParent` event here, as we already did it in `validateBeaconBlock()` // // TODO(fulu): is this note above correct? Could have random column that we see that could trigger // unknownBlockSync. And duplicate addition of a block will be deduplicated by the // BlockInputSync event handler. Check this!! // events.emit(NetworkEvent.unknownBlockParent, {blockInput, peer: peerIdStr}); } throw e; } finally { verificationTimer?.(); } } async function validatePayloadDataColumn( dataColumnSidecar: gloas.DataColumnSidecar, gossipSubnet: SubnetID, peerIdStr: string, seenTimestampSec: number ): Promise<PayloadEnvelopeInput> { metrics?.peerDas.dataColumnSidecarProcessingRequests.inc(); const slot = dataColumnSidecar.slot; const blockRootHex = toRootHex(dataColumnSidecar.beaconBlockRoot); // check to see if payload has already been processed and PayloadEnvelopeInput has been deleted (column received via reqresp or other means) if (chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.FULL) !== null) { metrics?.peerDas.dataColumnSidecarProcessingSkip.inc(); logger.debug("Already processed payload for column sidecar, skipping processing", { slot, blockRoot: blockRootHex, index: dataColumnSidecar.index, }); throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { code: DataColumnSidecarErrorCode.ALREADY_KNOWN, columnIndex: dataColumnSidecar.index, slot, }); } const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); if (!payloadInput) { // This should not happen for gossip because the network processor queues `data_column_sidecar` // until block import creates the corresponding PayloadEnvelopeInput. throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { code: DataColumnSidecarErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING, slot, blockRoot: blockRootHex, }); } // [IGNORE] The sidecar is the first sidecar for the tuple // (sidecar.beacon_block_root, sidecar.index) with valid kzg proof. if (payloadInput.hasColumn(dataColumnSidecar.index)) { metrics?.peerDas.dataColumnSidecarProcessingSkip.inc(); logger.debug("Already have column sidecar in PayloadEnvelopeInput, skipping processing", { ...payloadInput.getLogMeta(), index: dataColumnSidecar.index, }); throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { code: DataColumnSidecarErrorCode.ALREADY_KNOWN, columnIndex: dataColumnSidecar.index, slot, }); } const verificationTimer = metrics?.peerDas.dataColumnSidecarGossipVerificationTime.startTimer(); const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec); const secFromSlot = chain.clock.secFromSlot(slot); const recvToValLatency = Date.now() / 1000 - seenTimestampSec; try { await validateGossipGloasDataColumnSidecar(chain, payloadInput, dataColumnSidecar, gossipSubnet, metrics); const addedColumn = payloadInput.addColumn({ columnSidecar: dataColumnSidecar, source: PayloadEnvelopeInputSource.gossip, seenTimestampSec, peerIdStr, }); if (!addedColumn) { metrics?.peerDas.dataColumnSidecarProcessingSkip.inc(); logger.debug("Already have column sidecar in PayloadEnvelopeInput, skipping processing", { ...payloadInput.getLogMeta(), index: dataColumnSidecar.index, }); throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { code: DataColumnSidecarErrorCode.ALREADY_KNOWN, columnIndex: dataColumnSidecar.index, slot, }); } const recvToValidation = Date.now() / 1000 - seenTimestampSec; const validationTime = recvToValidation - recvToValLatency; metrics?.peerDas.dataColumnSidecarProcessingSuccesses.inc(); metrics?.gossipBlob.recvToValidation.observe(recvToValidation); metrics?.gossipBlob.validationTime.observe(validationTime); if (chain.emitter.listenerCount(routes.events.EventType.dataColumnSidecar)) { chain.emitter.emit(routes.events.EventType.dataColumnSidecar, { blockRoot: blockRootHex, slot, index: dataColumnSidecar.index, }); } logger.debug("Received gossip dataColumn", { ...payloadInput.getLogMeta(), currentSlot: chain.clock.currentSlot, peerId: peerIdStr, delaySec, secFromSlot, gossipSubnet, columnIndex: dataColumnSidecar.index, recvToValLatency, recvToValidation, validationTime, }); return payloadInput; } catch (e) { if (e instanceof DataColumnSidecarGossipError && e.action === GossipAction.REJECT) { chain.persistInvalidSszValue( sszTypesFor(payloadInput.forkName as ForkPostGloas).DataColumnSidecar, dataColumnSidecar, `gossip_reject_slot_${slot}_index_${dataColumnSidecar.index}` ); } throw e; } finally { verificationTimer?.(); } } function handleValidBeaconBlock(blockInput: IBlockInput, peerIdStr: string, seenTimestampSec: number): void { const signedBlock = blockInput.getBlock(); const slot = signedBlock.message.slot; // Handler - MUST NOT `await`, to allow validation result to be propagated const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime); metrics?.gossipBlock.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec); chain.validatorMonitor?.registerBeaconBlock(OpSource.gossip, delaySec, signedBlock.message); if (!blockInput.hasBlockAndAllData()) { chain.logger.debug("Received gossip block, attempting fetch of unavailable data", blockInput.getLogMeta()); // The data is not yet fully available, immediately trigger an aggressive pull via unknown block sync chain.emitter.emit(ChainEvent.incompleteBlockInput, { blockInput, peer: peerIdStr, source: BlockInputSource.gossip, }); // immediately attempt fetch of data columns from execution engine chain.getBlobsTracker.triggerGetBlobs(blockInput); } else { metrics?.blockInputFetchStats.totalDataAvailableBlockInputs.inc(); const blobCount = getBlobKzgCommitments( blockInput.forkName, signedBlock as SignedBeaconBlock<ForkPostDeneb> ).length; metrics?.blockInputFetchStats.totalDataAvailableBlockInputBlobs.inc(blobCount); } chain .processBlock(blockInput, { // block may be downloaded and processed by UnknownBlockSync ignoreIfKnown: true, // proposer signature already checked in validateBeaconBlock() validProposerSignature: true, // blobSidecars already checked in validateGossipBlobSidecars() validBlobSidecars: BlobSidecarValidation.Individual, // It's critical to keep a good number of mesh peers. // To do that, the Gossip Job Wait Time should be consistently <3s to avoid the behavior penalties in gossip // Gossip Job Wait Time depends on the BLS Job Wait Time // so `blsVerifyOnMainThread = true`: we want to verify signatures immediately without affecting the bls thread pool. // otherwise we can't utilize bls thread pool capacity and Gossip Job Wait Time can't be kept low consistently. // See https://github.com/ChainSafe/lodestar/issues/3792 blsVerifyOnMainThread: true, // to track block process steps seenTimestampSec, }) .then(() => { // Returns the delay between the start of `block.slot` and `current time` const delaySec = chain.clock.secFromSlot(slot); metrics?.gossipBlock.elapsedTimeTillProcessed.observe(delaySec); if (isForkPostGloas(blockInput.forkName)) { const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockInput.blockRootHex); // This payloadInput should have been created just after gossip validation if (!payloadInput) { throw Error( `PayloadEnvelopeInput not seeded for block ${blockInput.blockRootHex} during gossip processing` ); } // Immediately attempt fetch of data columns from execution engine as the bid contains kzg commitments // which is all the information we need so there is no reason to delay until execution payload arrives // TODO GLOAS: If we want EL retries after this initial attempt, add an explicit retry policy here // (for example later in the slot). Do not couple retries to incoming gossip columns. // Columns fetched here feed payloadInput.addColumn, which resolves waitForAllData for any // in-flight importExecutionPayload. No processExecutionPayload trigger needed from this path. chain.getBlobsTracker.triggerGetBlobs(payloadInput); } }) .catch((e) => { // Adjust verbosity based on error type let logLevel: LogLevel; if (e instanceof BlockError) { switch (e.type.code) { case BlockErrorCode.DATA_UNAVAILABLE: { // Error is quite frequent and not critical logLevel = LogLevel.debug; break; } // ALREADY_KNOWN should not happen with ignoreIfKnown=true above // PARENT_UNKNOWN should not happen, we handled this in validateBeaconBlock() function above case BlockErrorCode.ALREADY_KNOWN: case BlockErrorCode.PARENT_UNKNOWN: case BlockErrorCode.PRESTATE_MISSING: case BlockErrorCode.EXECUTION_ENGINE_ERROR: // Errors might indicate an issue with our node or the connected EL client logLevel = LogLevel.error; break; default: // TODO: Should it use PeerId or string? core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipBlock"); // Misbehaving peer, but could highlight an issue in another client logLevel = LogLevel.warn; } } else { // Any unexpected error logLevel = LogLevel.error; } metrics?.gossipBlock.processBlockErrors.inc({error: e instanceof BlockError ? e.type.code : "NOT_BLOCK_ERROR"}); logger[logLevel]( "Error processing block", {slot, peer: peerIdStr, blockRoot: prettyBytes(blockInput.blockRootHex)}, e as Error ); // TODO(fulu): Revisit when we prune block inputs chain.seenBlockInputCache.prune(blockInput.blockRootHex); }); } return { [GossipType.beacon_block]: async ({ gossipData, topic, peerIdStr, seenTimestampSec, }: GossipHandlerParamGeneric<GossipType.beacon_block>) => { const {serializedData} = gossipData; const signedBlock = sszDeserialize(topic, serializedData); const blockInput = await validateBeaconBlock(signedBlock, topic.boundary.fork, peerIdStr, seenTimestampSec); chain.serializedCache.set(signedBlock, serializedData); handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec); }, [GossipType.blob_sidecar]: async ({ gossipData, topic, peerIdStr, seenTimestampSec, }: GossipHandlerParamGeneric<GossipType.blob_sidecar>) => { const {serializedData} = gossipData; const blobSidecar = sszDeserialize(topic, serializedData); const blobSlot = blobSidecar.signedBlockHeader.message.slot; const index = blobSidecar.index; if (config.getForkSeq(blobSlot) < ForkSeq.deneb) { throw new GossipActionError(GossipAction.REJECT, {code: "PRE_DENEB_BLOCK"}); } const blockInput = await validateBeaconBlob(blobSidecar, topic.subnet, peerIdStr, seenTimestampSec); chain.serializedCache.set(blobSidecar, serializedData); if (!blockInput.hasBlockAndAllData()) { const cutoffTimeMs = getCutoffTimeMs(chain, blobSlot, BLOCK_AVAILABILITY_CUTOFF_MS); chain.logger.debug("Received gossip blob, waiting for full data availability", { msToWait: cutoffTimeMs, blobIndex: index, ...blockInput.getLogMeta(), }); blockInput.waitForAllData(cutoffTimeMs).catch((_e) => { chain.logger.debug( "Waited for data after receiving gossip blob. Cut-off reached so attempting to fetch remainder of BlockInput", { blobIndex: index, ...blockInput.getLogMeta(), } ); chain.emitter.emit(ChainEvent.incompleteBlockInput, { blockInput, peer: peerIdStr, source: BlockInputSource.gossip, }); }); } }, [GossipType.data_column_sidecar]: async ({ gossipData, topic, peerIdStr, seenTimestampSec, }: GossipHandlerParamGeneric<GossipType.data_column_sidecar>) => { const {fork} = topic.boundary; const {serializedData} = gossipData; const dataColumnSidecar = sszDeserialize(topic, serializedData); const dataColumnSlot = getDataColumnSidecarSlot(dataColumnSidecar); const index = dataColumnSidecar.index; const delaySec = chain.clock.secFromSlot(dataColumnSlot, seenTimestampSec); if (isForkPostGloas(fork)) { if (!isGloasDataColumnSidecar(dataColumnSidecar)) { throw new DataColumnSidecarGossipError(GossipAction.REJECT, { code: DataColumnSidecarErrorCode.INCORRECT_TYPE, slot: dataColumnSlot, columnIndex: index, fork, }); } // After gloas, data columns are tracked in PayloadEnvelopeInput const payloadInput = await validatePayloadDataColumn( dataColumnSidecar, topic.subnet, peerIdStr, seenTimestampSec ); chain.serializedCache.set(dataColumnSidecar, serializedData); const payloadInputMeta = payloadInput.getLogMeta(); const {receivedColumns} = payloadInputMeta; // it's not helpful to track every single column received // instead of that, track 1st, 8th, 16th 32th, 64th, and 128th column switch (receivedColumns) { case 1: case config.SAMPLES_PER_SLOT: case 2 * config.SAMPLES_PER_SLOT: case NUMBER_OF_COLUMNS / 4: case NUMBER_OF_COLUMNS / 2: case NUMBER_OF_COLUMNS: metrics?.dataColumns.elapsedTimeTillReceived.observe({receivedOrder: receivedColumns}, delaySec); break; } if (!payloadInput.hasComputedAllData()) { // if we've received at least half of the columns, trigger reconstruction of the rest if (receivedColumns >= NUMBER_OF_COLUMNS / 2) { chain.columnReconstructionTracker.triggerColumnReconstruction(payloadInput); } chain.logger.debug("Received gossip data column, payload envelope input not yet complete", { dataColumnIndex: index, ...payloadInputMeta, }); } // NOTE: we do NOT call chain.processExecutionPayload here. That is triggered only by // envelope arrival (gossip or API). An in-flight importExecutionPayload is awaiting // payloadInput.waitForAllData(); addColumn above will resolve it once hasAllData flips. if (!payloadInput.isComplete()) { const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS); // do not await here to not delay gossip validation payloadInput.waitForEnvelopeAndAllData(cutoffTimeMs).catch((_e) => { chain.logger.debug( "Waited for envelope and data after receiving gossip column. Cut-off reached so emitting incompletePayloadEnvelope", { dataColumnIndex: index, ...payloadInputMeta, } ); // TODO GLOAS: UnknownBlockSync to handle this event chain.emitter.emit(ChainEvent.incompletePayloadEnvelope, { payloadInput, peer: peerIdStr, source: BlockInputSource.gossip, }); }); } } else { if (config.getForkSeq(dataColumnSlot) < ForkSeq.fulu) { throw new GossipActionError(GossipAction.REJECT, {code: "PRE_FULU_BLOCK"}); } if (isGloasDataColumnSidecar(dataColumnSidecar)) { throw new DataColumnSidecarGossipError(GossipAction.REJECT, { code: DataColumnSidecarErrorCode.INCORRECT_TYPE, slot: dataColumnSlot, columnIndex: index, fork, }); } // Before gloas, data columns are tracked in BlockInput const blockInput = await validateBeaconDataColumn( dataColumnSidecar, serializedData, topic.subnet, peerIdStr, seenTimestampSec ); chain.serializedCache.set(dataColumnSidecar, serializedData); const blockInputMeta = blockInput.getLogMeta(); const {receivedColumns} = blockInputMeta; // it's not helpful to track every single column received // instead of that, track 1st, 8th, 16th 32th, 64th, and 128th column switch (receivedColumns) { case 1: case config.SAMPLES_PER_SLOT: case 2 * config.SAMPLES_PER_SLOT: case NUMBER_OF_COLUMNS / 4: case NUMBER_OF_COLUMNS / 2: case NUMBER_OF_COLUMNS: metrics?.dataColumns.elapsedTimeTillReceived.observe({receivedOrder: receivedColumns}, delaySec); break; } if (!blockInput.hasComputedAllData()) { // immediately attempt fetch of data columns from execution engine chain.getBlobsTracker.triggerGetBlobs(blockInput); // if we've received at least half of the columns, trigger reconstruction of the rest if (blockInput.columnCount >= NUMBER_OF_COLUMNS / 2) { chain.columnReconstructionTracker.triggerColumnReconstruction(blockInput); } } if (!blockInput.hasBlockAndAllData()) { const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS); chain.logger.debug("Received gossip data column, waiting for full data availability", { msToWait: cutoffTimeMs, dataColumnIndex: index, ...blockInputMeta, }); // do not await here to not delay gossip validation blockInput.waitForBlockAndAllData(cutoffTimeMs).catch((_e) => { chain.logger.debug( "Waited for data after receiving gossip column. Cut-off reached so attempting to fetch remainder of BlockInput", { dataColumnIndex: index, ...blockInputMeta, } ); chain.emitter.emit(ChainEvent.incompleteBlockInput, { blockInput, peer: peerIdStr, source: BlockInputSource.gossip, }); }); } } }, [GossipType.beacon_aggregate_and_proof]: async ({ gossipData, topic, seenTimestampSec, }: GossipHandlerParamGeneric<GossipType.beacon_aggregate_and_proof>) => { const {serializedData} = gossipData; let validationResult: AggregateAndProofValidationResult; const signedAggregateAndProof = sszDeserialize(topic, serializedData); const {fork} = topic.boundary; try { validationResult = await validateGossipAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData); } catch (e) { if (e instanceof AttestationError && e.action === GossipAction.REJECT) { chain.persistInvalidSszValue( sszTypesFor(fork).SignedAggregateAndProof, signedAggregateAndProof, "gossip_reject" ); } throw e; } // Handler const {indexedAttestation, committeeValidatorIndices, attDataRootHex} = validationResult; chain.validatorMonitor?.registerGossipAggregatedAttestation( seenTimestampSec, signedAggregateAndProof, indexedAttestation ); const aggregatedAttestation = signedAggregateAndProof.message.aggregate; const insertOutcome = chain.aggregatedAttestationPool.add( aggregatedAttestation, attDataRootHex, indexedAttestation.attestingIndices.length, committeeValidatorIndices ); metrics?.opPool.aggregatedAttestationPool.gossipInsertOutcome.inc({insertOutcome}); if (!options.dontSendGossipAttestationsToForkchoice) { try { chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex); } catch (e) { logger.debug( "Error adding gossip aggregated attestation to forkchoice", {slot: aggregatedAttestation.data.slot}, e as Error ); } } chain.emitter.emit(routes.events.EventType.attestation, signedAggregateAndProof.message.aggregate); }, [GossipType.attester_slashing]: async ({ gossipData, topic, }: GossipHandlerParamGeneric<GossipType.attester_slashing>) => { const {serializedData} = gossipData; const {fork} = topic.boundary; const attesterSlashing = sszDeserialize(topic, serializedData); await validateGossipAttesterSlashing(chain, attesterSlashing); // Handler try { chain.opPool.insertAttesterSlashing(fork, attesterSlashing); chain.forkChoice.onAttesterSlashing(attesterSlashing); } catch (e) { logger.error("Error adding attesterSlashing to pool", {}, e as Error); } chain.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing); }, [GossipType.proposer_slashing]: async ({ gossipData, topic, }: GossipHandlerParamGeneric<GossipType.proposer_slashing>) => { const {serializedData} = gossipData; const proposerSlashing = sszDeserialize(topic, serializedData); await validateGossipProposerSlashing(chain, proposerSlashing); // Handler try { chain.opPool.insertProposerSlashing(proposerSlashing); } catch (e) { logger.error("Error adding attesterSlashing to pool", {}, e as Error); } chain.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing); }, [GossipType.voluntary_exit]: async ({gossipData, topic}: GossipHandlerParamGeneric<GossipType.voluntary_exit>) => { const {serializedData} = gossipData; const voluntaryExit = sszDeserialize(topic, serializedData); await validateGossipVoluntaryExit(chain, voluntaryExit); // Handler try { chain.opPool.insertVoluntaryExit(voluntaryExit); } catch (e) { logger.error("Error adding voluntaryExit to pool", {}, e as Error); } chain.emitter.emit(routes.events.EventType.voluntaryExit, voluntaryExit); }, [GossipType.sync_committee_contribution_and_proof]: async ({ gossipData, topic, }: GossipHandlerParamGeneric<GossipType.sync_committee_contribution_and_proof>) => { const {serializedData} = gossipData; const contributionAndProof = sszDeserialize(topic, serializedData); const {syncCommitteeParticipantIndices} = await validateSyncCommitteeGossipContributionAndProof( chain, contributionAndProof ).catch((e) => { if (e instanceof SyncCommitteeError && e.action === GossipAction.REJECT) { chain.persistInvalidSszValue(ssz.altair.SignedContributionAndProof, contributionAndProof, "gossip_reject"); } throw e; }); // Handler chain.validatorMonitor?.registerGossipSyncContributionAndProof( contributionAndProof.message, syncCommitteeParticipantIndices ); try { const insertOutcome = chain.syncContributionAndProofPool.add( contributionAndProof.message, syncCommitteeParticipantIndices.length ); metrics?.opPool.syncContributionAndProofPool.gossipInsertOutcome.inc({insertOutcome}); } catch (e) { logger.error("Error adding to contributionAndProof pool", {}, e as Error); } chain.emitter.emit(routes.events.EventType.contributionAndProof, contributionAndProof); }, [GossipType.sync_committee]: async ({gossipData, topic}: GossipHandlerParamGeneric<GossipType.sync_committee>) => { const {serializedData} = gossipData; const syncCommittee = sszDeserialize(topic, serializedData); const {subnet} = topic; let indicesInSubcommittee: number[] = [0]; try { indicesInSubcommittee = (await validateGossipSyncCommittee(chain, syncCommittee, subnet)).indicesInSubcommittee; } catch (e) { if (e instanceof SyncCommitteeError && e.action === GossipAction.REJECT) { chain.persistInvalidSszValue(ssz.altair.SyncCommitteeMessage, syncCommittee, "gossip_reject"); } throw e; } // Handler — add for ALL positions this validator holds in the subcommittee try { for (const indexInSubcommittee of indicesInSubcommittee) { const insertOutcome = chain.syncCommitteeMessagePool.add(subnet, syncCommittee, indexInSubcommittee); metrics?.opPool.syncCommitteeMessagePoolInsertOutcome.inc({insertOutcome}); } } catch (e) { logger.debug("Error adding to syncCommittee pool", {subnet}, e as Error); } }, [GossipType.light_client_finality_update]: async ({ gossipData, topic, }: GossipHandlerParamGeneric<GossipType.light_client_finality_update>) => { const {serializedData} = gossipData; const lightClientFinalityUpdate = sszDeserialize(topic, serializedData); validateLightClientFinalityUpdate(config, chain, lightClientFinalityUpdate); }, [GossipType.light_client_optimistic_update]: async ({ gossipData, topic, }: GossipHandlerParamGeneric<GossipType.light_client_optimistic_update>) => { const {serializedData} = gossipData; const lightClientOptimisticUpdate = sszDeserialize(topic, serializedData); validateLightClientOptimisticUpdate(config, chain, lightClientOptimisticUpdate); }, // blsToExecutionChange is to be generated and validated against GENESIS_FORK_VERSION [GossipType.bls_to_execution_change]: async ({ gossipData, topic, }: GossipHandlerParamGeneric<GossipType.bls_to_execution_change>) => { const {serializedData} = gossipData; const blsToExecutionChange = sszDeserialize(topic, serializedData); await validateGossipBlsToExecutionChange(chain, blsToExecutionChange); // Handler try { chain.opPool.insertBlsToExecutionChange(blsToExecutionChange); } catch (e) { logger.error("Error adding blsToExecutionChange to pool", {}, e as Error); } chain.emitter.emit(routes.events.EventType.blsToExecutionChange, blsToExecutionChange); }, [GossipType.execution_payload]: async ({ gossipData, topic, peerIdStr, seenTimestampSec, }: GossipHandlerParamGeneric<GossipType.execution_payload>) => { const {serializedData} = gossipData; const signedEnvelope = sszDeserialize(topic, serializedData); const envelope = signedEnvelope.message; // unlike BlockInput, we send the envelope into UnknownBlockInput sync // inside the sync it'll reconcile into PayloadEnvelopeInput and share the same cache with gossip try { await validateGossipExecutionPayloadEnvelope(chain, signedEnvelope); } catch (e) { if (e instanceof ExecutionPayloadEnvelopeError) { const {beaconBlockRoot} = signedEnvelope.message; const slot = signedEnvelope.message.payload.slotNumber; logger.debug("Gossip envelope has error", {slot, root: toRootHex(beaconBlockRoot), code: e.type.code}); if (e.action === GossipAction.REJECT) { chain.persistInvalidSszValue( ssz.gloas.SignedExecutionPayloadEnvelope, signedEnvelope, `gossip_reject_slot_${slot}` ); } } throw e; } const slot = envelope.payload.slotNumber; const delaySec = seenTimestampSec - computeTimeAtSlot(config, slot, chain.genesisTime); metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec); chain.validatorMonitor?.registerExecutionPayloadEnvelope(OpSource.gossip, delaySec, signedEnvelope); const blockRootHex = toRootHex(envelope.beaconBlockRoot); const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); if (!payloadInput) { // This shouldn't happen because beacon block should have been imported and thus payload input should have been created. throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING, blockRoot: blockRootHex, }); } chain.serializedCache.set(signedEnvelope, serializedData); payloadInput.addPayloadEnvelope({ envelope: signedEnvelope, source: PayloadEnvelopeInputSource.gossip, seenTimestampSec, peerIdStr, }); chain.emitter.emit(routes.events.EventType.executionPayloadGossip, { slot, builderIndex: envelope.builderIndex, blockHash: toRootHex(envelope.payload.blockHash), blockRoot: blockRootHex, }); chain.processExecutionPayload(payloadInput, {validSignature: true}).catch((e) => { // Adjust verbosity based on error type let logLevel: LogLevel; if (e instanceof PayloadError) { switch (e.type.code) { // BLOCK_NOT_IN_FORK_CHOICE should not happen, validateGossipExecutionPayloadEnvelope above // already verified the block is in fork choice case PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE: case PayloadErrorCode.MISS_BLOCK_STATE: case PayloadErrorCode.EXECUTION_ENGINE_ERROR: // Errors might indicate an issue with our node or the connected EL client logLevel = LogLevel.error; break; // INVALID_SIGNATURE should not happen, signature is verified during gossip validation case PayloadErrorCode.INVALID_SIGNATURE: case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR: case PayloadErrorCode.EXECUTION_ENGINE_INVALID: core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipPayload"); // Misbehaving peer, but could highlight an issue in another client logLevel = LogLevel.warn; break; } } else { // Any unexpected error logLevel = LogLevel.error; } metrics?.gossipExecutionPayloadEnvelope.processPayloadErrors.inc({ error: e instanceof PayloadError ? e.type.code : "NOT_PAYLOAD_ERROR", }); chain.logger[logLevel]( "Error processing execution payload from gossip", {slot, peer: peerIdStr, root: blockRootHex}, e as Error ); }); }, [GossipType.payload_attestation_message]: async ({ gossipData, topic, }: GossipHandlerParamGeneric<GossipType.payload_attestation_message>) => { const {serializedData} = gossipData; const payloadAttestationMessage = sszDeserialize(topic, serializedData); const validationResult = await validateGossipPayloadAttestationMessage(chain, payloadAttestationMessage); try { const insertOutcome = chain.payloadAttestationPool.add( payloadAttestationMessage, validationResult.attDataRootHex, validationResult.validatorCommitteeIndex ); metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({insertOutcome}); } catch (e) { logger.error("Error adding to payloadAttestation pool", {}, e as Error); } chain.forkChoice.notifyPtcMessages( toRootHex(payloadAttestationMessage.data.beaconBlockRoot), [validationResult.validatorCommitteeIndex], payloadAttestationMessage.data.payloadPresent ); }, [GossipType.execution_payload_bid]: async ({ gossipData, topic, }: GossipHandlerParamGeneric<GossipType.execution_payload_bid>) => { const {serializedData} = gossipData; const executionPayloadBid = sszDeserialize(topic, serializedData); await validateGossipExecutionPayloadBid(chain, executionPayloadBid); // Handle valid payload bid by storing in a bid pool try { const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid.message); metrics?.opPool.executionPayloadBidPool.gossipInsertOutcome.inc({insertOutcome}); } catch (e) { logger.error("Error adding to executionPayloadBid pool", {}, e as Error); } chain.emitter.emit(routes.events.EventType.executionPayloadBid, { version: config.getForkName(exe