UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

283 lines (249 loc) • 10.8 kB
import {Signature, aggregateSignatures} from "@chainsafe/blst"; import {BitArray} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import {MAX_COMMITTEES_PER_SLOT, isForkPostElectra} from "@lodestar/params"; import {Attestation, RootHex, SingleAttestation, Slot, isElectraSingleAttestation} from "@lodestar/types"; import {MapDef, assert} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {IClock} from "../../util/clock.js"; import {InsertOutcome, OpPoolError, OpPoolErrorCode} from "./types.js"; import {isElectraAggregate, pruneBySlot, signatureFromBytesNoCheck} from "./utils.js"; /** * The number of slots that will be stored in the pool. * * For example, if `SLOTS_RETAINED == 3` and the pool is pruned at slot `6`, then all attestations * at slots less than `4` will be dropped and any future attestation with a slot less than `4` * will be refused. */ const SLOTS_RETAINED = 3; /** * The maximum number of distinct `AttestationData` that will be stored in each slot. * * This is a DoS protection measure. */ const MAX_ATTESTATIONS_PER_SLOT = 16_384; type AggregateFastPhase0 = { data: Attestation["data"]; aggregationBits: BitArray; signature: Signature; }; export type AggregateFastElectra = AggregateFastPhase0 & {committeeBits: BitArray}; export type AggregateFast = AggregateFastPhase0 | AggregateFastElectra; /** Hex string of DataRoot `TODO` */ type DataRootHex = string; /** CommitteeIndex must be null for pre-electra. Must not be null post-electra */ type CommitteeIndex = number | null; /** * A pool of `Attestation` that is specially designed to store "unaggregated" attestations from * the native aggregation scheme. * * **The `NaiveAggregationPool` does not do any signature or attestation verification. It assumes * that all `Attestation` objects provided are valid.** * * ## Details * * The pool sorts the `Attestation` by `attestation.data.slot`, then by `attestation.data`. * * As each unaggregated attestation is added it is aggregated with any existing `attestation` with * the same `AttestationData`. Considering that the pool only accepts attestations with a single * signature, there should only ever be a single aggregated `Attestation` for any given * `AttestationData`. * * The pool has a capacity for `SLOTS_RETAINED` slots, when a new `attestation.data.slot` is * provided, the oldest slot is dropped and replaced with the new slot. The pool can also be * pruned by supplying a `current_slot`; all existing attestations with a slot lower than * `current_slot - SLOTS_RETAINED` will be removed and any future attestation with a slot lower * than that will also be refused. Pruning is done automatically based upon the attestations it * receives and it can be triggered manually. */ export class AttestationPool { private readonly aggregateByIndexByRootBySlot = new MapDef< Slot, Map<DataRootHex, Map<CommitteeIndex, AggregateFast>> >(() => new Map<DataRootHex, Map<CommitteeIndex, AggregateFast>>()); private lowestPermissibleSlot = 0; constructor( private readonly config: ChainForkConfig, private readonly clock: IClock, private readonly preaggregateSlotDistance = 0, private readonly metrics: Metrics | null = null ) {} /** Returns current count of pre-aggregated attestations with unique data */ getAttestationCount(): number { let attestationCount = 0; for (const attestationByIndexByRoot of this.aggregateByIndexByRootBySlot.values()) { for (const attestationByIndex of attestationByIndexByRoot.values()) { attestationCount += attestationByIndex.size; } } return attestationCount; } /** * Accepts an `VerifiedUnaggregatedAttestation` and attempts to apply it to the "naive * aggregation pool". * * The naive aggregation pool is used by local validators to produce * `SignedAggregateAndProof`. * * If the attestation is too old (low slot) to be included in the pool it is simply dropped * and no error is returned. Also if it's at clock slot but come to the pool later than AGGREGATE_DUE_BPS * of slot time, it's dropped too since it's not helpful for the validator anymore * * Expects the attestation to be fully validated: * - Valid signature * - Consistent bitlength * - Valid committeeIndex * - Valid data */ add( committeeIndex: CommitteeIndex, attestation: SingleAttestation, attDataRootHex: RootHex, validatorCommitteeIndex: number, committeeSize: number, priority?: boolean ): InsertOutcome { const slot = attestation.data.slot; const fork = this.config.getForkName(slot); const lowestPermissibleSlot = this.lowestPermissibleSlot; // Reject any attestations that are too old. if (slot < lowestPermissibleSlot) { return InsertOutcome.Old; } // Reject gossip attestations in the current slot but come to this pool very late // for api attestations, we allow them to be added to the pool if (!priority && this.clock.msFromSlot(slot) > this.config.getAggregateDueMs(fork)) { return InsertOutcome.Late; } // Limit object per slot const aggregateByRoot = this.aggregateByIndexByRootBySlot.getOrDefault(slot); if (aggregateByRoot.size >= MAX_ATTESTATIONS_PER_SLOT) { throw new OpPoolError({code: OpPoolErrorCode.REACHED_MAX_PER_SLOT}); } if (isForkPostElectra(fork)) { // Electra only: this should not happen because attestation should be validated before reaching this assert.notNull(committeeIndex, "Committee index should not be null in attestation pool post-electra"); assert.true(isElectraSingleAttestation(attestation), "Attestation should be type electra.SingleAttestation"); } else { assert.true(!isElectraSingleAttestation(attestation), "Attestation should be type phase0.Attestation"); committeeIndex = null; // For pre-electra, committee index info is encoded in attDataRootIndex } // Pre-aggregate the contribution with existing items let aggregateByIndex = aggregateByRoot.get(attDataRootHex); if (aggregateByIndex === undefined) { aggregateByIndex = new Map<CommitteeIndex, AggregateFast>(); aggregateByRoot.set(attDataRootHex, aggregateByIndex); } const aggregate = aggregateByIndex.get(committeeIndex); if (aggregate) { // Aggregate mutating return aggregateAttestationInto(aggregate, attestation, validatorCommitteeIndex); } // Create new aggregate aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation, validatorCommitteeIndex, committeeSize)); return InsertOutcome.NewData; } /** * For validator API to get an aggregate */ getAggregate(slot: Slot, dataRootHex: RootHex, committeeIndex: CommitteeIndex): Attestation | null { const fork = this.config.getForkName(slot); const isPostElectra = isForkPostElectra(fork); committeeIndex = isPostElectra ? committeeIndex : null; const aggregate = this.aggregateByIndexByRootBySlot.get(slot)?.get(dataRootHex)?.get(committeeIndex); if (!aggregate) { this.metrics?.opPool.attestationPool.getAggregateCacheMisses.inc(); return null; } if (isPostElectra) { assert.true(isElectraAggregate(aggregate), "Aggregate should be type AggregateFastElectra"); } else { assert.true(!isElectraAggregate(aggregate), "Aggregate should be type AggregateFastPhase0"); } return fastToAttestation(aggregate); } /** * Removes any attestations with a slot lower than `current_slot - preaggregateSlotDistance`. * By default, not interested in attestations in old slots, we only preaggregate attestations for the current slot. */ prune(clockSlot: Slot): void { pruneBySlot(this.aggregateByIndexByRootBySlot, clockSlot, SLOTS_RETAINED); // by default preaggregateSlotDistance is 0, i.e only accept attestations in the same clock slot. this.lowestPermissibleSlot = Math.max(clockSlot - this.preaggregateSlotDistance, 0); } /** * Get all attestations optionally filtered by `attestation.data.slot` * @param bySlot slot to filter, `bySlot === attestation.data.slot` */ getAll(bySlot?: Slot): Attestation[] { const attestations: Attestation[] = []; const aggregateByRoots = bySlot === undefined ? Array.from(this.aggregateByIndexByRootBySlot.values()) : [this.aggregateByIndexByRootBySlot.get(bySlot)]; for (const aggregateByRoot of aggregateByRoots) { if (aggregateByRoot) { for (const aggFastByIndex of aggregateByRoot.values()) { for (const aggFast of aggFastByIndex.values()) { attestations.push(fastToAttestation(aggFast)); } } } } return attestations; } } // - Retrieve agg attestations by slot and data root // - Insert attestations coming from gossip and API /** * Aggregate a new attestation into `aggregate` mutating it */ function aggregateAttestationInto( aggregate: AggregateFast, attestation: SingleAttestation, validatorCommitteeIndex: number ): InsertOutcome { let bitIndex: number | null; if (isElectraSingleAttestation(attestation)) { bitIndex = validatorCommitteeIndex; } else { bitIndex = attestation.aggregationBits.getSingleTrueBit(); } // Should never happen, attestations are verified against this exact condition before assert.notNull(bitIndex, "Invalid attestation in pool, not exactly one bit set"); if (aggregate.aggregationBits.get(bitIndex) === true) { return InsertOutcome.AlreadyKnown; } aggregate.aggregationBits.set(bitIndex, true); aggregate.signature = aggregateSignatures([aggregate.signature, signatureFromBytesNoCheck(attestation.signature)]); return InsertOutcome.Aggregated; } /** * Format `contribution` into an efficient `aggregate` to add more contributions in with aggregateContributionInto() */ function attestationToAggregate( attestation: SingleAttestation, validatorCommitteeIndex: number, committeeSize: number ): AggregateFast { if (isElectraSingleAttestation(attestation)) { return { data: attestation.data, aggregationBits: BitArray.fromSingleBit(committeeSize, validatorCommitteeIndex), committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, attestation.committeeIndex), signature: signatureFromBytesNoCheck(attestation.signature), }; } return { data: attestation.data, // clone because it will be mutated aggregationBits: attestation.aggregationBits.clone(), signature: signatureFromBytesNoCheck(attestation.signature), }; } /** * Unwrap AggregateFast to Attestation */ function fastToAttestation(aggFast: AggregateFast): Attestation { return {...aggFast, signature: aggFast.signature.toBytes()}; }