UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

857 lines 57.9 kB
import { routes } from "@lodestar/api"; import { PayloadStatus } from "@lodestar/fork-choice"; import { ForkSeq, NUMBER_OF_COLUMNS, isForkPostElectra, isForkPostGloas, } from "@lodestar/params"; import { computeTimeAtSlot } from "@lodestar/state-transition"; import { isGloasDataColumnSidecar, ssz, sszTypesFor, } from "@lodestar/types"; import { LogLevel, prettyBytes, toHex, toRootHex } from "@lodestar/utils"; import { BlockInputSource, isBlockInputColumns, } from "../../chain/blocks/blockInput/index.js"; import { PayloadError, PayloadErrorCode } from "../../chain/blocks/importExecutionPayload.js"; import { 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 { 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 { 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 { kzgCommitmentToVersionedHash } from "../../util/blobs.js"; import { getBlobKzgCommitments, getDataColumnSidecarSlot } from "../../util/dataColumns.js"; import { GossipType, } from "../gossip/interface.js"; import { sszDeserialize } from "../gossip/topic.js"; import { PeerAction } from "../peers/index.js"; 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, options) { 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, options) { const { chain, config, metrics, logger, core } = modules; async function validateBeaconBlock(signedBlock, fork, peerIdStr, seenTimestampSec) { 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, 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, subnet, peerIdStr, seenTimestampSec) { 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; 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, _dataColumnBytes, gossipSubnet, peerIdStr, seenTimestampSec) { 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, gossipSubnet, peerIdStr, seenTimestampSec) { 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).DataColumnSidecar, dataColumnSidecar, `gossip_reject_slot_${slot}_index_${dataColumnSidecar.index}`); } throw e; } finally { verificationTimer?.(); } } function handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec) { 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).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; 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); // TODO(fulu): Revisit when we prune block inputs chain.seenBlockInputCache.prune(blockInput.blockRootHex); }); } return { [GossipType.beacon_block]: async ({ gossipData, topic, peerIdStr, seenTimestampSec, }) => { 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, }) => { 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, }) => { 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, }) => { const { serializedData } = gossipData; let validationResult; 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); } } chain.emitter.emit(routes.events.EventType.attestation, signedAggregateAndProof.message.aggregate); }, [GossipType.attester_slashing]: async ({ gossipData, topic, }) => { 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); } chain.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing); }, [GossipType.proposer_slashing]: async ({ gossipData, topic, }) => { 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); } chain.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing); }, [GossipType.voluntary_exit]: async ({ gossipData, topic }) => { 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); } chain.emitter.emit(routes.events.EventType.voluntaryExit, voluntaryExit); }, [GossipType.sync_committee_contribution_and_proof]: async ({ gossipData, topic, }) => { 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); } chain.emitter.emit(routes.events.EventType.contributionAndProof, contributionAndProof); }, [GossipType.sync_committee]: async ({ gossipData, topic }) => { const { serializedData } = gossipData; const syncCommittee = sszDeserialize(topic, serializedData); const { subnet } = topic; let indicesInSubcommittee = [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); } }, [GossipType.light_client_finality_update]: async ({ gossipData, topic, }) => { const { serializedData } = gossipData; const lightClientFinalityUpdate = sszDeserialize(topic, serializedData); validateLightClientFinalityUpdate(config, chain, lightClientFinalityUpdate); }, [GossipType.light_client_optimistic_update]: async ({ gossipData, topic, }) => { 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, }) => { 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); } chain.emitter.emit(routes.events.EventType.blsToExecutionChange, blsToExecutionChange); }, [GossipType.execution_payload]: async ({ gossipData, topic, peerIdStr, seenTimestampSec, }) => { 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; 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.lo