@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
260 lines • 11.1 kB
JavaScript
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