UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

221 lines • 10.3 kB
import { aggregateSignatures } from "@chainsafe/blst"; import { BitArray } from "@chainsafe/ssz"; import { MAX_COMMITTEES_PER_SLOT, isForkPostElectra } from "@lodestar/params"; import { isElectraSingleAttestation } from "@lodestar/types"; import { assert, MapDef } from "@lodestar/utils"; 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; /** * 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 { constructor(config, clock, cutOffSecFromSlot, preaggregateSlotDistance = 0, metrics = null) { this.config = config; this.clock = clock; this.cutOffSecFromSlot = cutOffSecFromSlot; this.preaggregateSlotDistance = preaggregateSlotDistance; this.metrics = metrics; this.aggregateByIndexByRootBySlot = new MapDef(() => new Map()); this.lowestPermissibleSlot = 0; } /** Returns current count of pre-aggregated attestations with unique data */ getAttestationCount() { 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 2/3 * 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, attestation, attDataRootHex, committeeValidatorIndex, committeeSize, priority) { 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.secFromSlot(slot) > this.cutOffSecFromSlot) { 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(); aggregateByRoot.set(attDataRootHex, aggregateByIndex); } const aggregate = aggregateByIndex.get(committeeIndex); if (aggregate) { // Aggregate mutating return aggregateAttestationInto(aggregate, attestation, committeeValidatorIndex); } // Create new aggregate aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation, committeeValidatorIndex, committeeSize)); return InsertOutcome.NewData; } /** * For validator API to get an aggregate */ getAggregate(slot, dataRootHex, committeeIndex) { 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) { 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) { const attestations = []; 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, attestation, committeeValidatorIndex) { let bitIndex; if (isElectraSingleAttestation(attestation)) { bitIndex = committeeValidatorIndex; } 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, committeeValidatorIndex, committeeSize) { if (isElectraSingleAttestation(attestation)) { return { data: attestation.data, aggregationBits: BitArray.fromSingleBit(committeeSize, committeeValidatorIndex), 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) { return { ...aggFast, signature: aggFast.signature.toBytes() }; } //# sourceMappingURL=attestationPool.js.map