UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

361 lines (323 loc) • 13.9 kB
import {ForkDigestContext} from "@lodestar/config"; import { ATTESTATION_SUBNET_COUNT, ForkName, ForkSeq, SYNC_COMMITTEE_SUBNET_COUNT, isForkPostAltair, isForkPostElectra, isForkPostFulu, } from "@lodestar/params"; import {Attestation, SingleAttestation, ssz, sszTypesFor} from "@lodestar/types"; import {GossipAction, GossipActionError, GossipErrorCode} from "../../chain/errors/gossipValidation.js"; import {NetworkConfig} from "../networkConfig.js"; import {DEFAULT_ENCODING} from "./constants.js"; import {GossipEncoding, GossipTopic, GossipTopicTypeMap, GossipType, SSZTypeOfGossipTopic} from "./interface.js"; export interface IGossipTopicCache { getTopic(topicStr: string): GossipTopic; } export class GossipTopicCache implements IGossipTopicCache { private topicsByTopicStr = new Map<string, Required<GossipTopic>>(); constructor(private readonly forkDigestContext: ForkDigestContext) {} /** Returns cached GossipTopic, otherwise attempts to parse it from the str */ getTopic(topicStr: string): GossipTopic { let topic = this.topicsByTopicStr.get(topicStr); if (topic === undefined) { topic = parseGossipTopic(this.forkDigestContext, topicStr); // TODO: Consider just throwing here. We should only receive messages from known subscribed topics this.topicsByTopicStr.set(topicStr, topic); } return topic; } /** Returns cached GossipTopic, otherwise returns undefined */ getKnownTopic(topicStr: string): GossipTopic | undefined { return this.topicsByTopicStr.get(topicStr); } setTopic(topicStr: string, topic: GossipTopic): void { if (!this.topicsByTopicStr.has(topicStr)) { this.topicsByTopicStr.set(topicStr, {encoding: DEFAULT_ENCODING, ...topic}); } } } /** * Stringify a GossipTopic into a spec-ed formated topic string */ export function stringifyGossipTopic(forkDigestContext: ForkDigestContext, topic: GossipTopic): string { const forkDigestHexNoPrefix = forkDigestContext.forkBoundary2ForkDigestHex(topic.boundary); const topicType = stringifyGossipTopicType(topic); const encoding = topic.encoding ?? DEFAULT_ENCODING; return `/eth2/${forkDigestHexNoPrefix}/${topicType}/${encoding}`; } /** * Stringify a GossipTopic into a spec-ed formated partial topic string */ function stringifyGossipTopicType(topic: GossipTopic): string { switch (topic.type) { case GossipType.beacon_block: case GossipType.beacon_aggregate_and_proof: case GossipType.voluntary_exit: case GossipType.proposer_slashing: case GossipType.attester_slashing: case GossipType.sync_committee_contribution_and_proof: case GossipType.light_client_finality_update: case GossipType.light_client_optimistic_update: case GossipType.bls_to_execution_change: case GossipType.execution_payload: case GossipType.payload_attestation_message: case GossipType.execution_payload_bid: case GossipType.proposer_preferences: return topic.type; case GossipType.beacon_attestation: case GossipType.sync_committee: return `${topic.type}_${topic.subnet}`; case GossipType.blob_sidecar: return `${topic.type}_${topic.subnet}`; case GossipType.data_column_sidecar: return `${topic.type}_${topic.subnet}`; } } export function getGossipSSZType(topic: GossipTopic) { const {fork} = topic.boundary; switch (topic.type) { case GossipType.beacon_block: // beacon_block is updated in altair to support the updated SignedBeaconBlock type return ssz[fork].SignedBeaconBlock; case GossipType.blob_sidecar: return ssz.deneb.BlobSidecar; case GossipType.data_column_sidecar: return isForkPostFulu(fork) ? sszTypesFor(fork).DataColumnSidecar : ssz.fulu.DataColumnSidecar; case GossipType.beacon_aggregate_and_proof: return sszTypesFor(fork).SignedAggregateAndProof; case GossipType.beacon_attestation: return sszTypesFor(fork).SingleAttestation; case GossipType.proposer_slashing: return ssz.phase0.ProposerSlashing; case GossipType.attester_slashing: return sszTypesFor(fork).AttesterSlashing; case GossipType.voluntary_exit: return ssz.phase0.SignedVoluntaryExit; case GossipType.sync_committee_contribution_and_proof: return ssz.altair.SignedContributionAndProof; case GossipType.sync_committee: return ssz.altair.SyncCommitteeMessage; case GossipType.light_client_optimistic_update: return isForkPostAltair(fork) ? sszTypesFor(fork).LightClientOptimisticUpdate : ssz.altair.LightClientOptimisticUpdate; case GossipType.light_client_finality_update: return isForkPostAltair(fork) ? sszTypesFor(fork).LightClientFinalityUpdate : ssz.altair.LightClientFinalityUpdate; case GossipType.bls_to_execution_change: return ssz.capella.SignedBLSToExecutionChange; case GossipType.execution_payload: return ssz.gloas.SignedExecutionPayloadEnvelope; case GossipType.payload_attestation_message: return ssz.gloas.PayloadAttestationMessage; case GossipType.execution_payload_bid: return ssz.gloas.SignedExecutionPayloadBid; case GossipType.proposer_preferences: return ssz.gloas.SignedProposerPreferences; } } /** * Deserialize a gossip serialized data into an ssz object. */ export function sszDeserialize<T extends GossipTopic>(topic: T, serializedData: Uint8Array): SSZTypeOfGossipTopic<T> { const sszType = getGossipSSZType(topic); try { return sszType.deserialize(serializedData) as SSZTypeOfGossipTopic<T>; } catch (_e) { throw new GossipActionError(GossipAction.REJECT, {code: GossipErrorCode.INVALID_SERIALIZED_BYTES_ERROR_CODE}); } } /** * @deprecated * Deserialize a gossip serialized data into an Attestation object. * No longer used post-electra. Use `sszDeserializeSingleAttestation` instead */ export function sszDeserializeAttestation(fork: ForkName, serializedData: Uint8Array): Attestation { try { return sszTypesFor(fork).Attestation.deserialize(serializedData); } catch (_e) { throw new GossipActionError(GossipAction.REJECT, {code: GossipErrorCode.INVALID_SERIALIZED_BYTES_ERROR_CODE}); } } /** * Deserialize a gossip seralized data into an SingleAttestation object. */ export function sszDeserializeSingleAttestation(fork: ForkName, serializedData: Uint8Array): SingleAttestation { try { if (isForkPostElectra(fork)) { return sszTypesFor(fork).SingleAttestation.deserialize(serializedData); } return sszTypesFor(fork).Attestation.deserialize(serializedData) as SingleAttestation; } catch (_e) { throw new GossipActionError(GossipAction.REJECT, {code: GossipErrorCode.INVALID_SERIALIZED_BYTES_ERROR_CODE}); } } // Parsing const gossipTopicRegex = /^\/eth2\/(\w+)\/(\w+)\/(\w+)/; /** * Parse a `GossipTopic` object from its stringified form. * A gossip topic has the format * ```ts * /eth2/$FORK_DIGEST/$GOSSIP_TYPE/$ENCODING * ``` */ export function parseGossipTopic(forkDigestContext: ForkDigestContext, topicStr: string): Required<GossipTopic> { try { const matches = topicStr.match(gossipTopicRegex); if (matches === null) { throw Error(`Must match regex ${gossipTopicRegex}`); } const [, forkDigestHexNoPrefix, gossipTypeStr, encodingStr] = matches; const boundary = forkDigestContext.forkDigest2ForkBoundary(forkDigestHexNoPrefix); const encoding = parseEncodingStr(encodingStr); // Inline-d the parseGossipTopicType() function since spreading the resulting object x4 the time to parse a topicStr switch (gossipTypeStr) { case GossipType.beacon_block: case GossipType.beacon_aggregate_and_proof: case GossipType.voluntary_exit: case GossipType.proposer_slashing: case GossipType.attester_slashing: case GossipType.sync_committee_contribution_and_proof: case GossipType.light_client_finality_update: case GossipType.light_client_optimistic_update: case GossipType.bls_to_execution_change: case GossipType.execution_payload: case GossipType.payload_attestation_message: case GossipType.execution_payload_bid: case GossipType.proposer_preferences: return {type: gossipTypeStr, boundary, encoding}; } for (const gossipType of [GossipType.beacon_attestation as const, GossipType.sync_committee as const]) { if (gossipTypeStr.startsWith(gossipType)) { const subnetStr = gossipTypeStr.slice(gossipType.length + 1); // +1 for '_' concatenating the topic name and the subnet const subnet = parseInt(subnetStr, 10); if (Number.isNaN(subnet)) throw Error(`Subnet ${subnetStr} is not a number`); return {type: gossipType, subnet, boundary, encoding}; } } if (gossipTypeStr.startsWith(GossipType.blob_sidecar)) { const subnetStr = gossipTypeStr.slice(GossipType.blob_sidecar.length + 1); // +1 for '_' concatenating the topic name and the subnet const subnet = parseInt(subnetStr, 10); if (Number.isNaN(subnet)) throw Error(`subnet ${subnetStr} is not a number`); return {type: GossipType.blob_sidecar, subnet, boundary, encoding}; } if (gossipTypeStr.startsWith(GossipType.data_column_sidecar)) { const subnetStr = gossipTypeStr.slice(GossipType.data_column_sidecar.length + 1); // +1 for '_' concatenating the topic name and the subnet const subnet = parseInt(subnetStr, 10); if (Number.isNaN(subnet)) throw Error(`subnet ${subnetStr} is not a number`); return {type: GossipType.data_column_sidecar, subnet, boundary, encoding}; } throw Error(`Unknown gossip type ${gossipTypeStr}`); } catch (e) { (e as Error).message = `Invalid gossip topic ${topicStr}: ${(e as Error).message}`; throw e; } } /** * De-duplicate logic to pick fork topics between subscribeCoreTopicsAtFork and unsubscribeCoreTopicsAtFork */ export function getCoreTopicsAtFork( networkConfig: NetworkConfig, fork: ForkName, opts: {subscribeAllSubnets?: boolean; disableLightClientServer?: boolean} ): GossipTopicTypeMap[keyof GossipTopicTypeMap][] { // Common topics for all forks const topics: GossipTopicTypeMap[keyof GossipTopicTypeMap][] = [ {type: GossipType.beacon_block}, {type: GossipType.beacon_aggregate_and_proof}, {type: GossipType.voluntary_exit}, {type: GossipType.proposer_slashing}, {type: GossipType.attester_slashing}, ]; if (ForkSeq[fork] >= ForkSeq.gloas) { topics.push({type: GossipType.execution_payload}); topics.push({type: GossipType.payload_attestation_message}); topics.push({type: GossipType.execution_payload_bid}); topics.push({type: GossipType.proposer_preferences}); } // After fulu also track data_column_sidecar_{index} if (ForkSeq[fork] >= ForkSeq.fulu) { topics.push(...getDataColumnSidecarTopics(networkConfig)); } // After Deneb and before Fulu also track blob_sidecar_{subnet_id} if (ForkSeq[fork] >= ForkSeq.deneb && ForkSeq[fork] < ForkSeq.fulu) { const {config} = networkConfig; const subnetCount = isForkPostElectra(fork) ? config.BLOB_SIDECAR_SUBNET_COUNT_ELECTRA : config.BLOB_SIDECAR_SUBNET_COUNT; for (let subnet = 0; subnet < subnetCount; subnet++) { topics.push({type: GossipType.blob_sidecar, subnet}); } } // capella if (ForkSeq[fork] >= ForkSeq.capella) { topics.push({type: GossipType.bls_to_execution_change}); } // Any fork after altair included if (ForkSeq[fork] >= ForkSeq.altair) { topics.push({type: GossipType.sync_committee_contribution_and_proof}); if (!opts.disableLightClientServer) { topics.push({type: GossipType.light_client_optimistic_update}); topics.push({type: GossipType.light_client_finality_update}); } } if (opts.subscribeAllSubnets) { for (let subnet = 0; subnet < ATTESTATION_SUBNET_COUNT; subnet++) { topics.push({type: GossipType.beacon_attestation, subnet}); } if (ForkSeq[fork] >= ForkSeq.altair) { for (let subnet = 0; subnet < SYNC_COMMITTEE_SUBNET_COUNT; subnet++) { topics.push({type: GossipType.sync_committee, subnet}); } } } return topics; } /** * Pick data column subnets to subscribe to post-fulu. */ export function getDataColumnSidecarTopics( networkConfig: NetworkConfig ): GossipTopicTypeMap[keyof GossipTopicTypeMap][] { const topics: GossipTopicTypeMap[keyof GossipTopicTypeMap][] = []; const subnets = networkConfig.custodyConfig.sampledSubnets; for (const subnet of subnets) { topics.push({type: GossipType.data_column_sidecar, subnet}); } return topics; } /** * Validate that a `encodingStr` is a known `GossipEncoding` */ function parseEncodingStr(encodingStr: string): GossipEncoding { switch (encodingStr) { case GossipEncoding.ssz_snappy: return encodingStr; default: throw Error(`Unknown encoding ${encodingStr}`); } } // TODO: Review which yes, and which not export const gossipTopicIgnoreDuplicatePublishError: Record<GossipType, boolean> = { [GossipType.beacon_block]: true, [GossipType.blob_sidecar]: true, [GossipType.data_column_sidecar]: true, [GossipType.beacon_aggregate_and_proof]: true, [GossipType.beacon_attestation]: true, [GossipType.voluntary_exit]: true, [GossipType.proposer_slashing]: false, // Why not this ones? [GossipType.attester_slashing]: false, [GossipType.sync_committee_contribution_and_proof]: true, [GossipType.sync_committee]: true, [GossipType.light_client_finality_update]: false, [GossipType.light_client_optimistic_update]: false, [GossipType.bls_to_execution_change]: true, [GossipType.execution_payload]: true, [GossipType.payload_attestation_message]: true, [GossipType.execution_payload_bid]: true, [GossipType.proposer_preferences]: true, };