UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

260 lines • 11.1 kB
import { ATTESTATION_SUBNET_COUNT, ForkSeq, SYNC_COMMITTEE_SUBNET_COUNT, isForkPostAltair, isForkPostElectra, } from "@lodestar/params"; import { ssz, sszTypesFor } from "@lodestar/types"; import { GossipAction, GossipActionError, GossipErrorCode } from "../../chain/errors/gossipValidation.js"; import { DEFAULT_ENCODING } from "./constants.js"; import { GossipEncoding, GossipType } from "./interface.js"; export class GossipTopicCache { constructor(forkDigestContext) { this.forkDigestContext = forkDigestContext; this.topicsByTopicStr = new Map(); } /** Returns cached GossipTopic, otherwise attempts to parse it from the str */ getTopic(topicStr) { 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) { return this.topicsByTopicStr.get(topicStr); } setTopic(topicStr, topic) { 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, topic) { 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) { 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: 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}`; } } export function getGossipSSZType(topic) { 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.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; } } /** * Deserialize a gossip serialized data into an ssz object. */ export function sszDeserialize(topic, serializedData) { const sszType = getGossipSSZType(topic); try { return sszType.deserialize(serializedData); } 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, serializedData) { 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, serializedData) { try { if (isForkPostElectra(fork)) { return sszTypesFor(fork).SingleAttestation.deserialize(serializedData); } return sszTypesFor(fork).Attestation.deserialize(serializedData); } 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, topicStr) { 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: return { type: gossipTypeStr, boundary, encoding }; } for (const gossipType of [GossipType.beacon_attestation, GossipType.sync_committee]) { 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 }; } throw Error(`Unknown gossip type ${gossipTypeStr}`); } catch (e) { e.message = `Invalid gossip topic ${topicStr}: ${e.message}`; throw e; } } /** * De-duplicate logic to pick fork topics between subscribeCoreTopicsAtFork and unsubscribeCoreTopicsAtFork */ export function getCoreTopicsAtFork(config, fork, opts) { // Common topics for all forks const topics = [ { type: GossipType.beacon_block }, { type: GossipType.beacon_aggregate_and_proof }, { type: GossipType.voluntary_exit }, { type: GossipType.proposer_slashing }, { type: GossipType.attester_slashing }, ]; // After Deneb also track blob_sidecar_{subnet_id} if (ForkSeq[fork] >= ForkSeq.deneb) { 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; } /** * Validate that a `encodingStr` is a known `GossipEncoding` */ function parseEncodingStr(encodingStr) { switch (encodingStr) { case GossipEncoding.ssz_snappy: return encodingStr; default: throw Error(`Unknown encoding ${encodingStr}`); } } // TODO: Review which yes, and which not export const gossipTopicIgnoreDuplicatePublishError = { [GossipType.beacon_block]: true, [GossipType.blob_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, }; //# sourceMappingURL=topic.js.map