@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
242 lines (213 loc) • 9.6 kB
text/typescript
import {Signature, aggregateSignatures} from "@chainsafe/blst";
import {BitArray} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {SYNC_COMMITTEE_SIZE, SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar/params";
import {G2_POINT_AT_INFINITY} from "@lodestar/state-transition";
import {Root, Slot, SubnetID, altair, ssz} from "@lodestar/types";
import {Logger, MapDef, toRootHex} from "@lodestar/utils";
import {Metrics} from "../../metrics/metrics.js";
import {IClock} from "../../util/clock.js";
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;
/**
* A one-one mapping to SyncContribution with fast data structure to help speed up the aggregation.
*/
export type SyncContributionFast = {
syncSubcommitteeBits: BitArray;
numParticipants: number;
syncSubcommitteeSignature: Uint8Array;
};
/** Hex string of `contribution.beaconBlockRoot` */
type BlockRootHex = string;
/**
* 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 {
private readonly bestContributionBySubnetRootBySlot = new MapDef<
Slot,
MapDef<BlockRootHex, Map<SubnetID, SyncContributionFast>>
>(() => new MapDef<BlockRootHex, Map<SubnetID, SyncContributionFast>>(() => new Map<number, SyncContributionFast>()));
private lowestPermissibleSlot = 0;
constructor(
private readonly config: ChainForkConfig,
private readonly clock: IClock,
private readonly metrics: Metrics | null = null,
private logger: Logger | null = null
) {
// 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(): number {
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: altair.ContributionAndProof,
syncCommitteeParticipants: number,
priority?: boolean
): InsertOutcome {
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: Slot, prevBlockRoot: Root): altair.SyncAggregate {
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: Slot): void {
pruneBySlot(this.bestContributionBySubnetRootBySlot, headSlot, SLOTS_RETAINED);
this.lowestPermissibleSlot = Math.max(headSlot - SLOTS_RETAINED, 0);
}
private onScrapeMetrics(metrics: Metrics): void {
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: SyncContributionFast,
newContribution: altair.SyncCommitteeContribution,
newNumParticipants: number
): InsertOutcome {
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: altair.SyncCommitteeContribution,
numParticipants: number
): SyncContributionFast {
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: Map<number, SyncContributionFast>,
metrics: Metrics | null = null
): altair.SyncAggregate {
// check for empty/undefined bestContributionBySubnet earlier
const syncCommitteeBits = BitArray.fromBitLen(SYNC_COMMITTEE_SIZE);
const signatures: Signature[] = [];
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(),
};
}