UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

146 lines (130 loc) 6.04 kB
import {CommitteeIndex, RootHex, Slot, SubnetID, phase0} from "@lodestar/types"; import {MapDef} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {InsertOutcome} from "../opPools/types.js"; export type SeenAttDataKey = AttDataBase64; // AttestationData is used to cache attestations type AttDataBase64 = string; export type AttestationDataCacheEntry = { // part of shuffling data, so this does not take memory committeeValidatorIndices: Uint32Array; committeeIndex: CommitteeIndex; // IndexedAttestationData signing root, 32 bytes signingRoot: Uint8Array; // to be consumed by forkchoice and oppool attDataRootHex: RootHex; // caching this for 3 slots take 600 instances max, this is nothing compared to attestations processed per slot // for example in a mainnet node subscribing to all subnets, attestations are processed up to 20k per slot attestationData: phase0.AttestationData; subnet: SubnetID; }; export enum RejectReason { // attestation data reaches MAX_CACHE_SIZE_PER_SLOT reached_limit = "reached_limit", // attestation data is too old too_old = "too_old", // attestation data is already known already_known = "already_known", } // For pre-electra, there is no committeeIndex in SingleAttestation, so we hard code it to 0 // AttDataBase64 has committeeIndex instead export const PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX = 0; /** * There are maximum 64 committees per slot, assuming 1 committee may have up to 3 different data due to some nodes * are not up to date, we can have up to 192 different attestation data per slot. */ const DEFAULT_MAX_CACHE_SIZE_PER_SLOT = 200; /** * It takes less than 300kb to cache 200 attestation data per slot, so we can cache 3 slots worth of attestation data. */ const DEFAULT_CACHE_SLOT_DISTANCE = 2; /** * Cached seen AttestationData to improve gossip validation. For Electra, this still take into account attestationIndex * even through it is moved outside of AttestationData. * As of April 2023, validating gossip attestation takes ~12% of cpu time for a node subscribing to all subnets on mainnet. * Having this cache help saves a lot of cpu time since most of the gossip attestations are on the same slot. */ export class SeenAttestationDatas { private cacheEntryByAttDataByIndexBySlot = new MapDef< Slot, MapDef<CommitteeIndex, Map<AttDataBase64, AttestationDataCacheEntry>> >( () => new MapDef<CommitteeIndex, Map<AttDataBase64, AttestationDataCacheEntry>>( () => new Map<AttDataBase64, AttestationDataCacheEntry>() ) ); private lowestPermissibleSlot = 0; constructor( private readonly metrics: Metrics | null, private readonly cacheSlotDistance = DEFAULT_CACHE_SLOT_DISTANCE, // mainly for unit test private readonly maxCacheSizePerSlot = DEFAULT_MAX_CACHE_SIZE_PER_SLOT ) { metrics?.seenCache.attestationData.totalSlot.addCollect(() => this.onScrapeLodestarMetrics(metrics)); } /** * Add an AttestationDataCacheEntry to the cache. * - preElectra: add(slot, PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, attDataBase64, cacheEntry) * - electra: add(slot, committeeIndex, attDataBase64, cacheEntry) */ add( slot: Slot, committeeIndex: CommitteeIndex, attDataBase64: AttDataBase64, cacheEntry: AttestationDataCacheEntry ): InsertOutcome { if (slot < this.lowestPermissibleSlot) { this.metrics?.seenCache.attestationData.reject.inc({reason: RejectReason.too_old}); return InsertOutcome.Old; } const cacheEntryByAttDataByIndex = this.cacheEntryByAttDataByIndexBySlot.getOrDefault(slot); const cacheEntryByAttData = cacheEntryByAttDataByIndex.getOrDefault(committeeIndex); if (cacheEntryByAttData.has(attDataBase64)) { this.metrics?.seenCache.attestationData.reject.inc({reason: RejectReason.already_known}); return InsertOutcome.AlreadyKnown; } if (cacheEntryByAttData.size >= this.maxCacheSizePerSlot) { this.metrics?.seenCache.attestationData.reject.inc({reason: RejectReason.reached_limit}); return InsertOutcome.ReachLimit; } cacheEntryByAttData.set(attDataBase64, cacheEntry); return InsertOutcome.NewData; } /** * Get an AttestationDataCacheEntry from the cache. * - preElectra: get(slot, PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, attDataBase64) * - electra: get(slot, committeeIndex, attDataBase64) */ get(slot: Slot, committeeIndex: CommitteeIndex, attDataBase64: SeenAttDataKey): AttestationDataCacheEntry | null { const cacheEntryByAttDataByIndex = this.cacheEntryByAttDataByIndexBySlot.get(slot); const cacheEntryByAttData = cacheEntryByAttDataByIndex?.get(committeeIndex); const cacheEntry = cacheEntryByAttData?.get(attDataBase64); if (cacheEntry) { this.metrics?.seenCache.attestationData.hit.inc(); } else { this.metrics?.seenCache.attestationData.miss.inc(); } return cacheEntry ?? null; } onSlot(clockSlot: Slot): void { this.lowestPermissibleSlot = Math.max(clockSlot - this.cacheSlotDistance, 0); for (const slot of this.cacheEntryByAttDataByIndexBySlot.keys()) { if (slot < this.lowestPermissibleSlot) { this.cacheEntryByAttDataByIndexBySlot.delete(slot); } } } private onScrapeLodestarMetrics(metrics: Metrics): void { metrics?.seenCache.attestationData.totalSlot.set(this.cacheEntryByAttDataByIndexBySlot.size); // tracking number of attestation data at current slot may not be correct if scrape time is not at the end of slot // so we track it at the previous slot const previousSlot = this.lowestPermissibleSlot + this.cacheSlotDistance - 1; const cacheEntryByAttDataByIndex = this.cacheEntryByAttDataByIndexBySlot.get(previousSlot); let count = 0; for (const cacheEntryByAttDataBase64 of cacheEntryByAttDataByIndex?.values() ?? []) { count += cacheEntryByAttDataBase64.size; } metrics?.seenCache.attestationData.countPerSlot.set(count); } }