UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

187 lines 9.1 kB
import { aggregateSignatures } from "@chainsafe/blst"; import { BitArray } from "@chainsafe/ssz"; import { SYNC_COMMITTEE_SIZE, SYNC_COMMITTEE_SUBNET_SIZE } from "@lodestar/params"; import { G2_POINT_AT_INFINITY } from "@lodestar/state-transition"; import { ssz } from "@lodestar/types"; import { MapDef, toRootHex } from "@lodestar/utils"; import { InsertOutcome, OpPoolError, OpPoolErrorCode } from "./types.js"; import { pruneBySlot, signatureFromBytesNoCheck } from "./utils.js"; /** * SyncCommittee aggregates are only useful for the next block they have signed. */ const SLOTS_RETAINED = 8; /** * The maximum number of distinct `SyncAggregateFast` that will be stored in each slot. * * This is a DoS protection measure. */ const MAX_ITEMS_PER_SLOT = 512; // SyncContributionAndProofPool constructor ensures SYNC_COMMITTEE_SUBNET_SIZE is multiple of 8 const SYNC_COMMITTEE_SUBNET_BYTES = SYNC_COMMITTEE_SUBNET_SIZE / 8; /** * Cache SyncCommitteeContribution and seen ContributionAndProof. * This is used for SignedContributionAndProof validation and block factory. * This stays in-memory and should be pruned per slot. */ export class SyncContributionAndProofPool { config; clock; metrics; logger; bestContributionBySubnetRootBySlot = new MapDef(() => new MapDef(() => new Map())); lowestPermissibleSlot = 0; constructor(config, clock, metrics = null, logger = null) { this.config = config; this.clock = clock; this.metrics = metrics; this.logger = logger; // Param guarantee for optimizations below that merge syncSubcommitteeBits as bytes if (SYNC_COMMITTEE_SUBNET_SIZE % 8 !== 0) { throw Error("SYNC_COMMITTEE_SUBNET_SIZE must be multiple of 8"); } metrics?.opPool.syncContributionAndProofPool.size.addCollect(() => this.onScrapeMetrics(metrics)); } /** Returns current count of unique SyncContributionFast by block root and subnet */ get size() { let count = 0; for (const bestContributionByRootBySubnet of this.bestContributionBySubnetRootBySlot.values()) { for (const bestContributionByRoot of bestContributionByRootBySubnet.values()) { count += bestContributionByRoot.size; } } return count; } /** * Only call this once we pass all validation. */ add(contributionAndProof, syncCommitteeParticipants, priority) { const { contribution } = contributionAndProof; const { slot, beaconBlockRoot } = contribution; const rootHex = toRootHex(beaconBlockRoot); // Reject if too old. if (slot < this.lowestPermissibleSlot) { return InsertOutcome.Old; } // Reject ContributionAndProofs of previous slots // for api ContributionAndProofs, we allow them to be added to the pool if (!priority && slot < this.clock.slotWithPastTolerance(this.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000)) { return InsertOutcome.Late; } // Limit object per slot const bestContributionBySubnetByRoot = this.bestContributionBySubnetRootBySlot.getOrDefault(slot); if (bestContributionBySubnetByRoot.size >= MAX_ITEMS_PER_SLOT) { throw new OpPoolError({ code: OpPoolErrorCode.REACHED_MAX_PER_SLOT }); } const bestContributionBySubnet = bestContributionBySubnetByRoot.getOrDefault(rootHex); const subnet = contribution.subcommitteeIndex; const bestContribution = bestContributionBySubnet.get(subnet); if (bestContribution) { return replaceIfBetter(bestContribution, contribution, syncCommitteeParticipants); } bestContributionBySubnet.set(subnet, contributionToFast(contribution, syncCommitteeParticipants)); return InsertOutcome.NewData; } /** * This is for producing blocks, the same to process_sync_committee_contributions in the spec. */ getAggregate(slot, prevBlockRoot) { const opPoolMetrics = this.metrics?.opPool.syncContributionAndProofPool; const bestContributionBySubnetByRoot = this.bestContributionBySubnetRootBySlot.getOrDefault(slot); opPoolMetrics?.getAggregateRoots.set(bestContributionBySubnetByRoot.size); const prevBlockRootHex = toRootHex(prevBlockRoot); const bestContributionBySubnet = bestContributionBySubnetByRoot.get(prevBlockRootHex) ?? new Map(); opPoolMetrics?.getAggregateSubnets.set(bestContributionBySubnet.size); if (bestContributionBySubnet.size === 0) { opPoolMetrics?.getAggregateReturnsEmpty.inc(); // this may happen, see https://github.com/ChainSafe/lodestar/issues/7299 const availableRoots = Array.from(bestContributionBySubnetByRoot.keys()).join(","); this.logger?.warn("SyncContributionAndProofPool.getAggregate: no contributions for root", { slot, root: prevBlockRootHex, availableRoots, }); // Must return signature as G2_POINT_AT_INFINITY when participating bits are empty // https://github.com/ethereum/consensus-specs/blob/30f2a076377264677e27324a8c3c78c590ae5e20/specs/altair/bls.md#eth2_fast_aggregate_verify return { syncCommitteeBits: ssz.altair.SyncCommitteeBits.defaultValue(), syncCommitteeSignature: G2_POINT_AT_INFINITY, }; } return aggregate(bestContributionBySubnet, this.metrics); } /** * Prune per head slot. * SyncCommittee aggregates are only useful for the next block they have signed. * We don't want to prune by clock slot in case there's a long period of skipped slots. */ prune(headSlot) { pruneBySlot(this.bestContributionBySubnetRootBySlot, headSlot, SLOTS_RETAINED); this.lowestPermissibleSlot = Math.max(headSlot - SLOTS_RETAINED, 0); } onScrapeMetrics(metrics) { const poolMetrics = metrics.opPool.syncContributionAndProofPool; poolMetrics.size.set(this.size); const previousSlot = this.clock.currentSlot - 1; const contributionBySubnetByBlockRoot = this.bestContributionBySubnetRootBySlot.getOrDefault(previousSlot); poolMetrics.blockRootsPerSlot.set(contributionBySubnetByBlockRoot.size); let index = 0; for (const contributionsBySubnet of contributionBySubnetByBlockRoot.values()) { let participationCount = 0; for (const contribution of contributionsBySubnet.values()) { participationCount += contribution.numParticipants; } poolMetrics.subnetsByBlockRoot.set({ index }, contributionsBySubnet.size); poolMetrics.participantsByBlockRoot.set({ index }, participationCount); index++; } } } /** * Mutate bestContribution if new contribution has more participants */ export function replaceIfBetter(bestContribution, newContribution, newNumParticipants) { const { numParticipants } = bestContribution; if (newNumParticipants <= numParticipants) { return InsertOutcome.NotBetterThan; } bestContribution.syncSubcommitteeBits = newContribution.aggregationBits; bestContribution.numParticipants = newNumParticipants; bestContribution.syncSubcommitteeSignature = newContribution.signature; return InsertOutcome.NewData; } /** * Format `contribution` into an efficient data structure to aggregate later. */ export function contributionToFast(contribution, numParticipants) { return { // No need to clone, aggregationBits are not mutated, only replaced syncSubcommitteeBits: contribution.aggregationBits, numParticipants, // No need to deserialize, signatures are not aggregated until when calling .getAggregate() syncSubcommitteeSignature: contribution.signature, }; } /** * Aggregate best contributions of each subnet into SyncAggregate * @returns SyncAggregate to be included in block body. */ export function aggregate(bestContributionBySubnet, metrics = null) { // check for empty/undefined bestContributionBySubnet earlier const syncCommitteeBits = BitArray.fromBitLen(SYNC_COMMITTEE_SIZE); const signatures = []; let participationCount = 0; for (const [subnet, bestContribution] of bestContributionBySubnet.entries()) { participationCount += bestContribution.numParticipants; const byteOffset = subnet * SYNC_COMMITTEE_SUBNET_BYTES; for (let i = 0; i < SYNC_COMMITTEE_SUBNET_BYTES; i++) { syncCommitteeBits.uint8Array[byteOffset + i] = bestContribution.syncSubcommitteeBits.uint8Array[i]; } signatures.push(signatureFromBytesNoCheck(bestContribution.syncSubcommitteeSignature)); } metrics?.opPool.syncContributionAndProofPool.getAggregateParticipants.set(participationCount); return { syncCommitteeBits, syncCommitteeSignature: aggregateSignatures(signatures).toBytes(), }; } //# sourceMappingURL=syncContributionAndProofPool.js.map