@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
361 lines (323 loc) • 13.9 kB
text/typescript
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,
};