From 8966fbb29d4c45f7239a7f1851caac01ea691f3f Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 18 Jul 2023 11:14:25 +0700 Subject: [PATCH] feat: validate gossip attestations same att data in batch --- .../src/chain/errors/attestationError.ts | 5 +- .../src/chain/validation/attestation.ts | 200 ++++++++++++--- .../src/metrics/metrics/lodestar.ts | 22 +- .../src/network/gossip/interface.ts | 53 ++-- .../src/network/processor/gossipHandlers.ts | 165 +++++++++---- .../network/processor/gossipQueues/index.ts | 64 ++--- .../network/processor/gossipQueues/types.ts | 2 +- .../network/processor/gossipValidatorFn.ts | 85 ++++++- .../src/network/processor/index.ts | 100 +++++--- .../src/network/processor/types.ts | 2 + packages/beacon-node/src/util/wrapError.ts | 2 +- .../test/e2e/network/gossipsub.test.ts | 24 +- .../beacon-node/test/perf/bls/bls.test.ts | 58 ++++- .../perf/chain/validation/attestation.test.ts | 87 +++++-- .../unit/chain/validation/attestation.test.ts | 227 ++++++++++++++++-- .../test/utils/validationData/attestation.ts | 9 +- .../src/util/signatureSets.ts | 32 +-- packages/state-transition/test/perf/util.ts | 14 +- 18 files changed, 891 insertions(+), 260 deletions(-) diff --git a/packages/beacon-node/src/chain/errors/attestationError.ts b/packages/beacon-node/src/chain/errors/attestationError.ts index a93f5b42e439..913827d4ca38 100644 --- a/packages/beacon-node/src/chain/errors/attestationError.ts +++ b/packages/beacon-node/src/chain/errors/attestationError.ts @@ -132,6 +132,8 @@ export enum AttestationErrorCode { INVALID_SERIALIZED_BYTES = "ATTESTATION_ERROR_INVALID_SERIALIZED_BYTES", /** Too many skipped slots. */ TOO_MANY_SKIPPED_SLOTS = "ATTESTATION_ERROR_TOO_MANY_SKIPPED_SLOTS", + /** attDataBase64 is not available */ + NO_INDEXED_DATA = "ATTESTATION_ERROR_NO_INDEXED_DATA", } export type AttestationErrorType = @@ -166,7 +168,8 @@ export type AttestationErrorType = | {code: AttestationErrorCode.INVALID_AGGREGATOR} | {code: AttestationErrorCode.INVALID_INDEXED_ATTESTATION} | {code: AttestationErrorCode.INVALID_SERIALIZED_BYTES} - | {code: AttestationErrorCode.TOO_MANY_SKIPPED_SLOTS; headBlockSlot: Slot; attestationSlot: Slot}; + | {code: AttestationErrorCode.TOO_MANY_SKIPPED_SLOTS; headBlockSlot: Slot; attestationSlot: Slot} + | {code: AttestationErrorCode.NO_INDEXED_DATA}; export class AttestationError extends GossipActionError { getMetadata(): Record { diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index bea4d060e48a..6e0efa34beeb 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -1,13 +1,14 @@ import {toHexString} from "@chainsafe/ssz"; +import bls from "@chainsafe/bls"; import {phase0, Epoch, Root, Slot, RootHex, ssz} from "@lodestar/types"; import {ProtoBlock} from "@lodestar/fork-choice"; import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, ForkName, ForkSeq} from "@lodestar/params"; import { computeEpochAtSlot, CachedBeaconStateAllForks, - ISignatureSet, getAttestationDataSigningRoot, createSingleSignatureSetFromComponents, + SingleSignatureSet, } from "@lodestar/state-transition"; import {IBeaconChain} from ".."; import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/index.js"; @@ -16,11 +17,19 @@ import {RegenCaller} from "../regen/index.js"; import { AttDataBase64, getAggregationBitsFromAttestationSerialized, - getAttDataBase64FromAttestationSerialized, getSignatureFromAttestationSerialized, } from "../../util/sszBytes.js"; import {AttestationDataCacheEntry} from "../seenCache/seenAttestationData.js"; import {sszDeserializeAttestation} from "../../network/gossip/topic.js"; +import {Result, wrapError} from "../../util/wrapError.js"; +import {MIN_SIGNATURE_SETS_TO_BATCH_VERIFY} from "../../network/processor/gossipQueues/index.js"; +import {signatureFromBytesNoCheck} from "../opPools/utils.js"; + +export type BatchResult = { + results: Result[]; + batchableBls: boolean; + fallbackBls: boolean; +}; export type AttestationValidationResult = { attestation: phase0.Attestation; @@ -40,6 +49,12 @@ export type GossipAttestation = { serializedData: Uint8Array; // available in NetworkProcessor since we check for unknown block root attestations attSlot: Slot; + attDataBase64?: string | null; +}; + +export type Phase0Result = AttestationValidationResult & { + signatureSet: SingleSignatureSet; + validatorIndex: number; }; /** @@ -49,11 +64,13 @@ export type GossipAttestation = { const SHUFFLING_LOOK_AHEAD_EPOCHS = 1; /** - * Validate attestations from gossip - * - Only deserialize the attestation if needed, use the cached AttestationData instead - * - This is to avoid deserializing similar attestation multiple times which could help the gc - * - subnet is required - * - do not prioritize bls signature set + * Verify gossip attestations of the same attestation data. + * - If there are less than 32 signatures, verify each signature individually with batchable = true + * - If there are not less than 32 signatures + * - do a quick verify by aggregate all signatures and pubkeys, this takes 4.6ms for 32 signatures and 7.6ms for 64 signatures + * - if one of the signature is invalid, do a fallback verify by verify each signature individually with batchable = false + * - subnet is required + * - do not prioritize bls signature set */ export async function validateGossipAttestation( fork: ForkName, @@ -65,6 +82,112 @@ export async function validateGossipAttestation( return validateAttestation(fork, chain, attestationOrBytes, subnet); } +export async function validateGossipAttestationsSameAttData( + fork: ForkName, + chain: IBeaconChain, + attestationOrBytesArr: AttestationOrBytes[], + subnet: number, + // for unit test, consumers do not need to pass this + phase0ValidationFn = validateGossipAttestationNoSignatureCheck +): Promise { + if (attestationOrBytesArr.length === 0) { + return {results: [], batchableBls: false, fallbackBls: false}; + } + + // phase0: do all verifications except for signature verification + const phase0ResultOrErrors = await Promise.all( + attestationOrBytesArr.map((attestationOrBytes) => + wrapError(phase0ValidationFn(fork, chain, attestationOrBytes, subnet)) + ) + ); + + // phase1: verify signatures of all valid attestations + // map new index to index in resultOrErrors + const newIndexToOldIndex = new Map(); + const signatureSets: SingleSignatureSet[] = []; + let newIndex = 0; + const phase0Results: Phase0Result[] = []; + for (const [i, resultOrError] of phase0ResultOrErrors.entries()) { + if (resultOrError.err) { + continue; + } + phase0Results.push(resultOrError.result); + newIndexToOldIndex.set(newIndex, i); + signatureSets.push(resultOrError.result.signatureSet); + newIndex++; + } + + let signatureValids: boolean[]; + let batchableBls = false; + let fallbackBls = false; + if (signatureSets.length >= MIN_SIGNATURE_SETS_TO_BATCH_VERIFY) { + // all signature sets should have same signing root since we filtered in network processor + const aggregatedPubkey = bls.PublicKey.aggregate(signatureSets.map((set) => set.pubkey)); + const aggregatedSignature = bls.Signature.aggregate( + // no need to check signature, will do a final verify later + signatureSets.map((set) => signatureFromBytesNoCheck(set.signature)) + ); + + // quick check, it's likely this is valid most of the time + batchableBls = true; + const isAllValid = aggregatedSignature.verify(aggregatedPubkey, signatureSets[0].signingRoot); + fallbackBls = !isAllValid; + signatureValids = isAllValid + ? new Array(signatureSets.length).fill(true) + : // batchable is false because one of the signature is invalid + await Promise.all(signatureSets.map((set) => chain.bls.verifySignatureSets([set], {batchable: false}))); + } else { + batchableBls = false; + // don't want to block the main thread if there are too few signatures + signatureValids = await Promise.all( + signatureSets.map((set) => chain.bls.verifySignatureSets([set], {batchable: true})) + ); + } + + // phase0 post validation + for (const [i, isValid] of signatureValids.entries()) { + const oldIndex = newIndexToOldIndex.get(i); + if (oldIndex == null) { + // should not happen + throw Error(`Cannot get old index for index ${i}`); + } + + const {validatorIndex, attestation} = phase0Results[i]; + const targetEpoch = attestation.data.target.epoch; + if (isValid) { + // Now that the attestation has been fully verified, store that we have received a valid attestation from this validator. + // + // It's important to double check that the attestation still hasn't been observed, since + // there can be a race-condition if we receive two attestations at the same time and + // process them in different threads. + if (chain.seenAttesters.isKnown(targetEpoch, validatorIndex)) { + phase0ResultOrErrors[oldIndex] = { + err: new AttestationError(GossipAction.IGNORE, { + code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN, + targetEpoch, + validatorIndex, + }), + }; + } + + // valid + chain.seenAttesters.add(targetEpoch, validatorIndex); + } else { + phase0ResultOrErrors[oldIndex] = { + err: new AttestationError(GossipAction.IGNORE, { + code: AttestationErrorCode.INVALID_SIGNATURE, + }), + }; + } + } + + return { + results: phase0ResultOrErrors, + batchableBls, + fallbackBls, + }; +} + /** * Validate attestations from api * - no need to deserialize attestation @@ -81,17 +204,42 @@ export async function validateApiAttestation( } /** - * Only deserialize the attestation if needed, use the cached AttestationData instead - * This is to avoid deserializing similar attestation multiple times which could help the gc + * Validate a single unaggregated attestation + * subnet is null for api attestations */ -async function validateAttestation( +export async function validateAttestation( fork: ForkName, chain: IBeaconChain, attestationOrBytes: AttestationOrBytes, - /** Optional, to allow verifying attestations through API with unknown subnet */ subnet: number | null, prioritizeBls = false ): Promise { + const phase0Result = await validateGossipAttestationNoSignatureCheck(fork, chain, attestationOrBytes, subnet); + const {attestation, signatureSet, validatorIndex} = phase0Result; + const isValid = await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}); + + if (isValid) { + const targetEpoch = attestation.data.target.epoch; + chain.seenAttesters.add(targetEpoch, validatorIndex); + return phase0Result; + } else { + throw new AttestationError(GossipAction.IGNORE, { + code: AttestationErrorCode.INVALID_SIGNATURE, + }); + } +} + +/** + * Only deserialize the attestation if needed, use the cached AttestationData instead + * This is to avoid deserializing similar attestation multiple times which could help the gc + */ +async function validateGossipAttestationNoSignatureCheck( + fork: ForkName, + chain: IBeaconChain, + attestationOrBytes: AttestationOrBytes, + /** Optional, to allow verifying attestations through API with unknown subnet */ + subnet: number | null +): Promise { // Do checks in this order: // - do early checks (w/o indexed attestation) // - > obtain indexed attestation and committes per slot @@ -108,8 +256,13 @@ async function validateAttestation( let attDataBase64: AttDataBase64 | null; if (attestationOrBytes.serializedData) { // gossip - attDataBase64 = getAttDataBase64FromAttestationSerialized(attestationOrBytes.serializedData); const attSlot = attestationOrBytes.attSlot; + if (!attestationOrBytes.attDataBase64) { + throw new AttestationError(GossipAction.IGNORE, { + code: AttestationErrorCode.NO_INDEXED_DATA, + }); + } + attDataBase64 = attestationOrBytes.attDataBase64; const cachedAttData = attDataBase64 !== null ? chain.seenAttestationDatas.get(attSlot, attDataBase64) : null; if (cachedAttData === null) { const attestation = sszDeserializeAttestation(attestationOrBytes.serializedData); @@ -263,7 +416,7 @@ async function validateAttestation( // [REJECT] The signature of attestation is valid. const attestingIndices = [validatorIndex]; - let signatureSet: ISignatureSet; + let signatureSet: SingleSignatureSet; let attDataRootHex: RootHex; const signature = attestationOrCache.attestation ? attestationOrCache.attestation.signature @@ -304,24 +457,7 @@ async function validateAttestation( } } - if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { - throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.INVALID_SIGNATURE}); - } - - // Now that the attestation has been fully verified, store that we have received a valid attestation from this validator. - // - // It's important to double check that the attestation still hasn't been observed, since - // there can be a race-condition if we receive two attestations at the same time and - // process them in different threads. - if (chain.seenAttesters.isKnown(targetEpoch, validatorIndex)) { - throw new AttestationError(GossipAction.IGNORE, { - code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN, - targetEpoch, - validatorIndex, - }); - } - - chain.seenAttesters.add(targetEpoch, validatorIndex); + // no signature check, leave that for phase1 const indexedAttestation: phase0.IndexedAttestation = { attestingIndices, @@ -336,7 +472,7 @@ async function validateAttestation( data: attData, signature, }; - return {attestation, indexedAttestation, subnet: expectedSubnet, attDataRootHex}; + return {attestation, indexedAttestation, subnet: expectedSubnet, attDataRootHex, signatureSet, validatorIndex}; } /** diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index 8b3410a6eadf..aae4ae0fe6ba 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -38,9 +38,9 @@ export function createLodestarMetrics( help: "Count of total gossip validation queue length", labelNames: ["topic"], }), - dropRatio: register.gauge<"topic">({ - name: "lodestar_gossip_validation_queue_current_drop_ratio", - help: "Current drop ratio of gossip validation queue", + keySize: register.gauge<"topic">({ + name: "lodestar_gossip_validation_queue_key_size", + help: "Count of total gossip validation queue key size", labelNames: ["topic"], }), droppedJobs: register.gauge<"topic">({ @@ -575,6 +575,22 @@ export function createLodestarMetrics( labelNames: ["caller"], buckets: [0, 1, 2, 4, 8, 16, 32, 64], }), + attestationBatchCount: register.gauge({ + name: "lodestar_gossip_attestation_verified_in_batch_count", + help: "Count of attestations verified in batch", + }), + attestationNonBatchCount: register.gauge({ + name: "lodestar_gossip_attestation_verified_non_batch_count", + help: "Count of attestations NOT verified in batch", + }), + totalBatch: register.gauge({ + name: "lodestar_gossip_attestation_total_batch_count", + help: "Total number of attestation batches", + }), + totalBatchFallbackBlsCheck: register.gauge({ + name: "lodestar_gossip_attestation_total_batch_fallback_bls_check_count", + help: "Total number of attestation batches that fallback to checking each signature separately", + }), }, // Gossip block diff --git a/packages/beacon-node/src/network/gossip/interface.ts b/packages/beacon-node/src/network/gossip/interface.ts index 58961bd6db2f..825178532ee8 100644 --- a/packages/beacon-node/src/network/gossip/interface.ts +++ b/packages/beacon-node/src/network/gossip/interface.ts @@ -7,6 +7,7 @@ import {BeaconConfig} from "@lodestar/config"; import {Logger} from "@lodestar/utils"; import {IBeaconChain} from "../../chain/index.js"; import {JobItemQueue} from "../../util/queue/index.js"; +import {AttestationError} from "../../chain/errors/attestationError.js"; export enum GossipType { beacon_block = "beacon_block", @@ -124,13 +125,18 @@ export type GossipModules = { * * js-libp2p-gossipsub expects validation functions that look like this */ -export type GossipValidatorFn = ( - topic: GossipTopic, - msg: Message, - propagationSource: PeerIdStr, - seenTimestampSec: number, - msgSlot: Slot | null -) => Promise; +export type GossipMessageInfo = { + topic: GossipTopic; + msg: Message; + propagationSource: PeerIdStr; + seenTimestampSec: number; + msgSlot: Slot | null; + indexed?: string; +}; + +export type GossipValidatorFn = (messageInfo: GossipMessageInfo) => Promise; + +export type GossipValidatorBatchFn = (messageInfos: GossipMessageInfo[]) => Promise; export type ValidatorFnsByType = {[K in GossipType]: GossipValidatorFn}; @@ -141,22 +147,31 @@ export type GossipJobQueues = { export type GossipData = { serializedData: Uint8Array; msgSlot?: Slot | null; + indexed?: string; +}; + +export type GossipHandlerParam = { + gossipData: GossipData; + topic: GossipTopicMap[GossipType]; + peerIdStr: string; + seenTimestampSec: number; }; -export type GossipHandlerFn = ( - gossipData: GossipData, - topic: GossipTopicMap[GossipType], - peerIdStr: string, - seenTimestampSec: number -) => Promise; +export type GossipHandlerFn = (gossipHandlerParam: GossipHandlerParam) => Promise; + +export type BatchGossipHandlerFn = (gossipHandlerParam: GossipHandlerParam[]) => Promise<(null | AttestationError)[]>; + +export type GossipHandlerParamGeneric = { + gossipData: GossipData; + topic: GossipTopicMap[T]; + peerIdStr: string; + seenTimestampSec: number; +}; export type GossipHandlers = { - [K in GossipType]: ( - gossipData: GossipData, - topic: GossipTopicMap[K], - peerIdStr: string, - seenTimestampSec: number - ) => Promise; + [K in GossipType]: + | ((gossipHandlerParam: GossipHandlerParamGeneric) => Promise) + | ((gossipHandlerParams: GossipHandlerParamGeneric[]) => Promise<(null | AttestationError)[]>); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index c0a91a0ae812..c08c9198ff8f 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -17,10 +17,9 @@ import { GossipActionError, SyncCommitteeError, } from "../../chain/errors/index.js"; -import {GossipHandlers, GossipType} from "../gossip/interface.js"; +import {GossipHandlerParamGeneric, GossipHandlers, GossipType} from "../gossip/interface.js"; import { validateGossipAggregateAndProof, - validateGossipAttestation, validateGossipAttesterSlashing, validateGossipBlock, validateGossipProposerSlashing, @@ -28,8 +27,9 @@ import { validateSyncCommitteeGossipContributionAndProof, validateGossipVoluntaryExit, validateGossipBlsToExecutionChange, - AttestationValidationResult, AggregateAndProofValidationResult, + validateGossipAttestationsSameAttData, + AttestationOrBytes, } from "../../chain/validation/index.js"; import {NetworkEvent, NetworkEventBus} from "../events.js"; import {PeerAction} from "../peers/index.js"; @@ -243,7 +243,14 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH } return { - [GossipType.beacon_block]: async ({serializedData}, topic, peerIdStr, seenTimestampSec) => { + [GossipType.beacon_block]: async ({ + gossipData, + topic, + peerIdStr, + seenTimestampSec, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; + const signedBlock = sszDeserialize(topic, serializedData); const blockInput = await validateBeaconBlock( signedBlock, @@ -263,7 +270,13 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH } }, - [GossipType.blob_sidecar]: async ({serializedData}, topic, peerIdStr, seenTimestampSec) => { + [GossipType.blob_sidecar]: async ({ + gossipData, + topic, + peerIdStr, + seenTimestampSec, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; const signedBlob = sszDeserialize(topic, serializedData); if (config.getForkSeq(signedBlob.message.slot) < ForkSeq.deneb) { throw new GossipActionError(GossipAction.REJECT, {code: "PRE_DENEB_BLOCK"}); @@ -280,7 +293,12 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH } }, - [GossipType.beacon_aggregate_and_proof]: async ({serializedData}, topic, _peer, seenTimestampSec) => { + [GossipType.beacon_aggregate_and_proof]: async ({ + gossipData, + topic, + seenTimestampSec, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; let validationResult: AggregateAndProofValidationResult; const signedAggregateAndProof = sszDeserialize(topic, serializedData); const {fork} = topic; @@ -320,56 +338,79 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH chain.emitter.emit(routes.events.EventType.attestation, signedAggregateAndProof.message.aggregate); }, - - [GossipType.beacon_attestation]: async ({serializedData, msgSlot}, topic, _peer, seenTimestampSec) => { - if (msgSlot == undefined) { - throw Error("msgSlot is undefined for beacon_attestation topic"); + [GossipType.beacon_attestation]: async ( + gossipHandlerParams: GossipHandlerParamGeneric[] + ) => { + const results: (null | AttestationError)[] = []; + if (gossipHandlerParams.length === 0) { + return results; } - const {subnet, fork} = topic; - - // do not deserialize gossipSerializedData here, it's done in validateGossipAttestation only if needed - let validationResult: AttestationValidationResult; - try { - validationResult = await validateGossipAttestation( - fork, - chain, - {attestation: null, serializedData, attSlot: msgSlot}, - subnet - ); - } catch (e) { - if (e instanceof AttestationError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszBytes(ssz.phase0.Attestation.typeName, serializedData, "gossip_reject"); + // all attestations should have same attestation data as filtered by network processor + const {subnet, fork} = gossipHandlerParams[0].topic; + const validationParams = gossipHandlerParams.map((param) => ({ + attestation: null, + serializedData: param.gossipData.serializedData, + attSlot: param.gossipData.msgSlot, + attDataBase64: param.gossipData.indexed, + })) as AttestationOrBytes[]; + const { + results: validationResults, + batchableBls, + fallbackBls, + } = await validateGossipAttestationsSameAttData(fork, chain, validationParams, subnet); + for (const [i, validationResult] of validationResults.entries()) { + if (validationResult.err) { + results.push(validationResult.err as AttestationError); + continue; } - throw e; - } - // Handler - const {indexedAttestation, attDataRootHex, attestation} = validationResult; - metrics?.registerGossipUnaggregatedAttestation(seenTimestampSec, indexedAttestation); + results.push(null); - try { - // Node may be subscribe to extra subnets (long-lived random subnets). For those, validate the messages - // but don't add to attestation pool, to save CPU and RAM - if (aggregatorTracker.shouldAggregate(subnet, indexedAttestation.data.slot)) { - const insertOutcome = chain.attestationPool.add(attestation, attDataRootHex); - metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome}); - } - } catch (e) { - logger.error("Error adding unaggregated attestation to pool", {subnet}, e as Error); - } + // Handler + const {indexedAttestation, attDataRootHex, attestation} = validationResult.result; + metrics?.registerGossipUnaggregatedAttestation(gossipHandlerParams[i].seenTimestampSec, indexedAttestation); - if (!options.dontSendGossipAttestationsToForkchoice) { try { - chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex); + // Node may be subscribe to extra subnets (long-lived random subnets). For those, validate the messages + // but don't add to attestation pool, to save CPU and RAM + if (aggregatorTracker.shouldAggregate(subnet, indexedAttestation.data.slot)) { + const insertOutcome = chain.attestationPool.add(attestation, attDataRootHex); + metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome}); + } } catch (e) { - logger.debug("Error adding gossip unaggregated attestation to forkchoice", {subnet}, e as Error); + logger.error("Error adding unaggregated attestation to pool", {subnet}, e as Error); + } + + if (!options.dontSendGossipAttestationsToForkchoice) { + try { + chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex); + } catch (e) { + logger.debug("Error adding gossip unaggregated attestation to forkchoice", {subnet}, e as Error); + } } + + chain.emitter.emit(routes.events.EventType.attestation, attestation); + } + + if (batchableBls) { + metrics?.gossipAttestation.totalBatch.inc(); + metrics?.gossipAttestation.attestationBatchCount.inc(gossipHandlerParams.length); + } else { + metrics?.gossipAttestation.attestationNonBatchCount.inc(gossipHandlerParams.length); + } + + if (fallbackBls) { + metrics?.gossipAttestation.totalBatchFallbackBlsCheck.inc(); } - chain.emitter.emit(routes.events.EventType.attestation, attestation); + return results; }, - [GossipType.attester_slashing]: async ({serializedData}, topic) => { + [GossipType.attester_slashing]: async ({ + gossipData, + topic, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; const attesterSlashing = sszDeserialize(topic, serializedData); await validateGossipAttesterSlashing(chain, attesterSlashing); @@ -383,7 +424,11 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH } }, - [GossipType.proposer_slashing]: async ({serializedData}, topic) => { + [GossipType.proposer_slashing]: async ({ + gossipData, + topic, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; const proposerSlashing = sszDeserialize(topic, serializedData); await validateGossipProposerSlashing(chain, proposerSlashing); @@ -396,7 +441,8 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH } }, - [GossipType.voluntary_exit]: async ({serializedData}, topic) => { + [GossipType.voluntary_exit]: async ({gossipData, topic}: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; const voluntaryExit = sszDeserialize(topic, serializedData); await validateGossipVoluntaryExit(chain, voluntaryExit); @@ -411,7 +457,11 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH chain.emitter.emit(routes.events.EventType.voluntaryExit, voluntaryExit); }, - [GossipType.sync_committee_contribution_and_proof]: async ({serializedData}, topic) => { + [GossipType.sync_committee_contribution_and_proof]: async ({ + gossipData, + topic, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; const contributionAndProof = sszDeserialize(topic, serializedData); const {syncCommitteeParticipantIndices} = await validateSyncCommitteeGossipContributionAndProof( chain, @@ -435,7 +485,8 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH chain.emitter.emit(routes.events.EventType.contributionAndProof, contributionAndProof); }, - [GossipType.sync_committee]: async ({serializedData}, topic) => { + [GossipType.sync_committee]: async ({gossipData, topic}: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; const syncCommittee = sszDeserialize(topic, serializedData); const {subnet} = topic; let indexInSubcommittee = 0; @@ -458,18 +509,30 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH } }, - [GossipType.light_client_finality_update]: async ({serializedData}, topic) => { + [GossipType.light_client_finality_update]: async ({ + gossipData, + topic, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; const lightClientFinalityUpdate = sszDeserialize(topic, serializedData); validateLightClientFinalityUpdate(config, chain, lightClientFinalityUpdate); }, - [GossipType.light_client_optimistic_update]: async ({serializedData}, topic) => { + [GossipType.light_client_optimistic_update]: async ({ + gossipData, + topic, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; const lightClientOptimisticUpdate = sszDeserialize(topic, serializedData); validateLightClientOptimisticUpdate(config, chain, lightClientOptimisticUpdate); }, // blsToExecutionChange is to be generated and validated against GENESIS_FORK_VERSION - [GossipType.bls_to_execution_change]: async ({serializedData}, topic) => { + [GossipType.bls_to_execution_change]: async ({ + gossipData, + topic, + }: GossipHandlerParamGeneric) => { + const {serializedData} = gossipData; const blsToExecutionChange = sszDeserialize(topic, serializedData); await validateGossipBlsToExecutionChange(chain, blsToExecutionChange); diff --git a/packages/beacon-node/src/network/processor/gossipQueues/index.ts b/packages/beacon-node/src/network/processor/gossipQueues/index.ts index c85b2560ebce..37eb354f8bb9 100644 --- a/packages/beacon-node/src/network/processor/gossipQueues/index.ts +++ b/packages/beacon-node/src/network/processor/gossipQueues/index.ts @@ -1,33 +1,36 @@ import {mapValues} from "@lodestar/utils"; import {GossipType} from "../../gossip/interface.js"; +import {PendingGossipsubMessage} from "../types.js"; +import {getAttDataBase64FromAttestationSerialized} from "../../../util/sszBytes.js"; import {LinearGossipQueue} from "./linear.js"; +import { + DropType, + GossipQueue, + GossipQueueOpts, + QueueType, + isIndexedGossipQueueAvgTimeOpts, + isIndexedGossipQueueMinSizeOpts, +} from "./types.js"; +import {IndexedGossipQueueMinSize} from "./indexed.js"; +import {IndexedGossipQueueAvgTime} from "./indexedAvgTime.js"; -export enum QueueType { - FIFO = "FIFO", - LIFO = "LIFO", -} - -export enum DropType { - count = "count", - ratio = "ratio", -} +/** + * In normal condition, the higher this value the more efficient the signature verification. + * However, if at least 1 signature is invalid, we need to verify each signature separately. + */ +const MAX_GOSSIP_ATTESTATION_BATCH_SIZE = 128; -type DropOpts = - | { - type: DropType.count; - count: number; - } - | { - type: DropType.ratio; - start: number; - step: number; - }; +/** + * Batching too few signatures and verifying them on main thread is not worth it, + * we should only batch verify when there are at least 32 signatures. + */ +export const MIN_SIGNATURE_SETS_TO_BATCH_VERIFY = 32; /** * Numbers from https://github.com/sigp/lighthouse/blob/b34a79dc0b02e04441ba01fd0f304d1e203d877d/beacon_node/network/src/beacon_processor/mod.rs#L69 */ const gossipQueueOpts: { - [K in GossipType]: GossipQueueOpts; + [K in GossipType]: GossipQueueOpts; } = { // validation gossip block asap [GossipType.beacon_block]: {maxLength: 1024, type: QueueType.FIFO, dropOpts: {type: DropType.count, count: 1}}, @@ -49,8 +52,9 @@ const gossipQueueOpts: { // start with dropping 1% of the queue, then increase 1% more each time. Reset when queue is empty [GossipType.beacon_attestation]: { maxLength: 24576, - type: QueueType.LIFO, - dropOpts: {type: DropType.ratio, start: 0.01, step: 0.01}, + indexFn: (item: PendingGossipsubMessage) => getAttDataBase64FromAttestationSerialized(item.msg.data), + minChunkSize: MIN_SIGNATURE_SETS_TO_BATCH_VERIFY, + maxChunkSize: MAX_GOSSIP_ATTESTATION_BATCH_SIZE, }, [GossipType.voluntary_exit]: {maxLength: 4096, type: QueueType.FIFO, dropOpts: {type: DropType.count, count: 1}}, [GossipType.proposer_slashing]: {maxLength: 4096, type: QueueType.FIFO, dropOpts: {type: DropType.count, count: 1}}, @@ -79,12 +83,6 @@ const gossipQueueOpts: { }, }; -type GossipQueueOpts = { - type: QueueType; - maxLength: number; - dropOpts: DropOpts; -}; - /** * Wraps a GossipValidatorFn with a queue, to limit the processing of gossip objects by type. * @@ -101,8 +99,14 @@ type GossipQueueOpts = { * By topic is too specific, so by type groups all similar objects in the same queue. All in the same won't allow * to customize different queue behaviours per object type (see `gossipQueueOpts`). */ -export function createGossipQueues(): {[K in GossipType]: LinearGossipQueue} { +export function createGossipQueues(): {[K in GossipType]: GossipQueue} { return mapValues(gossipQueueOpts, (opts) => { - return new LinearGossipQueue(opts); + if (isIndexedGossipQueueMinSizeOpts(opts)) { + return new IndexedGossipQueueMinSize(opts); + } else if (isIndexedGossipQueueAvgTimeOpts(opts)) { + return new IndexedGossipQueueAvgTime(opts); + } else { + return new LinearGossipQueue(opts); + } }); } diff --git a/packages/beacon-node/src/network/processor/gossipQueues/types.ts b/packages/beacon-node/src/network/processor/gossipQueues/types.ts index 074eebfb5219..9d6e41c7579e 100644 --- a/packages/beacon-node/src/network/processor/gossipQueues/types.ts +++ b/packages/beacon-node/src/network/processor/gossipQueues/types.ts @@ -1,4 +1,4 @@ -export type GossipQueueOpts = LinearGossipQueueOpts | IndexedGossipQueueOpts; +export type GossipQueueOpts = LinearGossipQueueOpts | IndexedGossipQueueOpts | IndexedGossipQueueMinSizeOpts; export type LinearGossipQueueOpts = { type: QueueType; diff --git a/packages/beacon-node/src/network/processor/gossipValidatorFn.ts b/packages/beacon-node/src/network/processor/gossipValidatorFn.ts index a3f4a4aa25b4..89c40b7482f0 100644 --- a/packages/beacon-node/src/network/processor/gossipValidatorFn.ts +++ b/packages/beacon-node/src/network/processor/gossipValidatorFn.ts @@ -2,8 +2,15 @@ import {TopicValidatorResult} from "@libp2p/interface/pubsub"; import {ChainForkConfig} from "@lodestar/config"; import {Logger} from "@lodestar/utils"; import {Metrics} from "../../metrics/index.js"; -import {GossipValidatorFn, GossipHandlers, GossipHandlerFn} from "../gossip/interface.js"; -import {GossipActionError, GossipAction} from "../../chain/errors/index.js"; +import { + GossipValidatorFn, + GossipHandlers, + GossipHandlerFn, + GossipValidatorBatchFn, + BatchGossipHandlerFn, + GossipMessageInfo, +} from "../gossip/interface.js"; +import {GossipActionError, GossipAction, AttestationError} from "../../chain/errors/index.js"; export type ValidatorFnModules = { config: ChainForkConfig; @@ -11,6 +18,68 @@ export type ValidatorFnModules = { metrics: Metrics | null; }; +/** + * Similar to getGossipValidatorFn but return a function to accept a batch of beacon_attestation messages + * with the same attestation data + */ +export function getGossipValidatorBatchFn( + gossipHandlers: GossipHandlers, + modules: ValidatorFnModules +): GossipValidatorBatchFn { + const {logger, metrics} = modules; + + return async function gossipValidatorBatchFn(messageInfos: GossipMessageInfo[]) { + // all messageInfos have same topic + const {topic} = messageInfos[0]; + const type = topic.type; + try { + const results = await (gossipHandlers[type] as BatchGossipHandlerFn)( + messageInfos.map((messageInfo) => ({ + gossipData: { + serializedData: messageInfo.msg.data, + msgSlot: messageInfo.msgSlot, + indexed: messageInfo.indexed, + }, + topic, + peerIdStr: messageInfo.propagationSource, + seenTimestampSec: messageInfo.seenTimestampSec, + })) + ); + + return results.map((e) => { + if (e == null) { + return TopicValidatorResult.Accept; + } + + if (!(e instanceof AttestationError)) { + logger.debug(`Gossip batch validation ${type} threw a non-AttestationError`, {}, e as Error); + metrics?.networkProcessor.gossipValidationIgnore.inc({topic: type}); + return TopicValidatorResult.Ignore; + } + + switch (e.action) { + case GossipAction.IGNORE: + metrics?.networkProcessor.gossipValidationIgnore.inc({topic: type}); + return TopicValidatorResult.Ignore; + + case GossipAction.REJECT: + metrics?.networkProcessor.gossipValidationReject.inc({topic: type}); + logger.debug(`Gossip validation ${type} rejected`, {}, e); + return TopicValidatorResult.Reject; + } + }); + } catch (e) { + // Don't expect error here + logger.debug(`Gossip batch validation ${type} threw an error`, {}, e as Error); + const results: TopicValidatorResult[] = []; + for (let i = 0; i < messageInfos.length; i++) { + results.push(TopicValidatorResult.Ignore); + } + return results; + } + }; +} + /** * Returns a GossipSub validator function from a GossipHandlerFn. GossipHandlerFn may throw GossipActionError if one * or more validation conditions from the consensus-specs#p2p-interface are not satisfied. @@ -28,16 +97,16 @@ export type ValidatorFnModules = { export function getGossipValidatorFn(gossipHandlers: GossipHandlers, modules: ValidatorFnModules): GossipValidatorFn { const {logger, metrics} = modules; - return async function gossipValidatorFn(topic, msg, propagationSource, seenTimestampSec, msgSlot) { + return async function gossipValidatorFn({topic, msg, propagationSource, seenTimestampSec, msgSlot}) { const type = topic.type; try { - await (gossipHandlers[type] as GossipHandlerFn)( - {serializedData: msg.data, msgSlot}, + await (gossipHandlers[type] as GossipHandlerFn)({ + gossipData: {serializedData: msg.data, msgSlot}, topic, - propagationSource, - seenTimestampSec - ); + peerIdStr: propagationSource, + seenTimestampSec, + }); metrics?.networkProcessor.gossipValidationAccept.inc({topic: type}); diff --git a/packages/beacon-node/src/network/processor/index.ts b/packages/beacon-node/src/network/processor/index.ts index 332a8577887d..2692d692a22f 100644 --- a/packages/beacon-node/src/network/processor/index.ts +++ b/packages/beacon-node/src/network/processor/index.ts @@ -8,13 +8,19 @@ import {Metrics} from "../../metrics/metrics.js"; import {IBeaconDb} from "../../db/interface.js"; import {ClockEvent} from "../../util/clock.js"; import {NetworkEvent, NetworkEventBus} from "../events.js"; -import {GossipHandlers, GossipType, GossipValidatorFn} from "../gossip/interface.js"; +import { + GossipHandlers, + GossipMessageInfo, + GossipType, + GossipValidatorBatchFn, + GossipValidatorFn, +} from "../gossip/interface.js"; import {PeerIdStr} from "../peers/index.js"; import {createGossipQueues} from "./gossipQueues/index.js"; import {PendingGossipsubMessage} from "./types.js"; import {ValidatorFnsModules, GossipHandlerOpts, getGossipHandlers} from "./gossipHandlers.js"; import {createExtractBlockSlotRootFns} from "./extractSlotRootFns.js"; -import {ValidatorFnModules, getGossipValidatorFn} from "./gossipValidatorFn.js"; +import {ValidatorFnModules, getGossipValidatorBatchFn, getGossipValidatorFn} from "./gossipValidatorFn.js"; export * from "./types.js"; @@ -142,7 +148,8 @@ export class NetworkProcessor { private readonly logger: Logger; private readonly metrics: Metrics | null; private readonly gossipValidatorFn: GossipValidatorFn; - private readonly gossipQueues = createGossipQueues(); + private readonly gossipValidatorBatchFn: GossipValidatorBatchFn; + private readonly gossipQueues = createGossipQueues(); private readonly gossipTopicConcurrency = mapValues(this.gossipQueues, () => 0); private readonly extractBlockSlotRootFns = createExtractBlockSlotRootFns(); // we may not receive the block for Attestation and SignedAggregateAndProof messages, in that case PendingGossipsubMessage needs @@ -163,6 +170,10 @@ export class NetworkProcessor { this.logger = logger; this.events = events; this.gossipValidatorFn = getGossipValidatorFn(modules.gossipHandlers ?? getGossipHandlers(modules, opts), modules); + this.gossipValidatorBatchFn = getGossipValidatorBatchFn( + modules.gossipHandlers ?? getGossipHandlers(modules, opts), + modules + ); events.on(NetworkEvent.pendingGossipsubMessage, this.onPendingGossipsubMessage.bind(this)); this.chain.emitter.on(routes.events.EventType.block, this.onBlockProcessed.bind(this)); @@ -179,7 +190,7 @@ export class NetworkProcessor { metrics.gossipValidationQueue.length.addCollect(() => { for (const topic of executeGossipWorkOrder) { metrics.gossipValidationQueue.length.set({topic}, this.gossipQueues[topic].length); - metrics.gossipValidationQueue.dropRatio.set({topic}, this.gossipQueues[topic].dropRatio); + metrics.gossipValidationQueue.keySize.set({topic}, this.gossipQueues[topic].keySize); metrics.gossipValidationQueue.concurrency.set({topic}, this.gossipTopicConcurrency[topic]); } metrics.reprocessGossipAttestations.countPerSlot.set(this.unknownBlockGossipsubMessagesCount); @@ -363,13 +374,14 @@ export class NetworkProcessor { } const item = this.gossipQueues[topic].next(); + const numMessages = Array.isArray(item) ? item.length : 1; if (item) { - this.gossipTopicConcurrency[topic]++; + this.gossipTopicConcurrency[topic] += numMessages; this.processPendingGossipsubMessage(item) - .finally(() => this.gossipTopicConcurrency[topic]--) + .finally(() => (this.gossipTopicConcurrency[topic] -= numMessages)) .catch((e) => this.logger.error("processGossipAttestations must not throw", {}, e)); - jobsSubmitted++; + jobsSubmitted += numMessages; // Attempt to find more work, but check canAcceptWork() again and run executeGossipWorkOrder priorization continue job_loop; } @@ -384,40 +396,68 @@ export class NetworkProcessor { } } - private async processPendingGossipsubMessage(message: PendingGossipsubMessage): Promise { - message.startProcessUnixSec = Date.now() / 1000; + private async processPendingGossipsubMessage( + messageOrArray: PendingGossipsubMessage | PendingGossipsubMessage[] + ): Promise { + const nowSec = Date.now() / 1000; + if (Array.isArray(messageOrArray)) { + messageOrArray.forEach((msg) => (msg.startProcessUnixSec = nowSec)); + } else { + messageOrArray.startProcessUnixSec = nowSec; + } - const acceptance = await this.gossipValidatorFn( - message.topic, - message.msg, - message.propagationSource, - message.seenTimestampSec, - message.msgSlot ?? null - ); + const acceptanceArr = Array.isArray(messageOrArray) + ? // for beacon_attestation topic, process attestations with same attestation data + // we always have msgSlot in beaccon_attestation topic so the type conversion is safe + await this.gossipValidatorBatchFn(messageOrArray as GossipMessageInfo[]) + : [ + // for other topics + await this.gossipValidatorFn({...messageOrArray, msgSlot: messageOrArray.msgSlot ?? null}), + ]; + + if (Array.isArray(messageOrArray)) { + messageOrArray.forEach((msg) => this.trackJobTime(msg, messageOrArray.length)); + } else { + this.trackJobTime(messageOrArray, 1); + } + // Use setTimeout to yield to the macro queue + // This is mostly due to too many attestation messages, and a gossipsub RPC may + // contain multiple of them. This helps avoid the I/O lag issue. + + if (Array.isArray(messageOrArray)) { + for (const [i, msg] of messageOrArray.entries()) { + setTimeout(() => { + this.events.emit(NetworkEvent.gossipMessageValidationResult, { + msgId: msg.msgId, + propagationSource: msg.propagationSource, + acceptance: acceptanceArr[i], + }); + }, 0); + } + } else { + setTimeout(() => { + this.events.emit(NetworkEvent.gossipMessageValidationResult, { + msgId: messageOrArray.msgId, + propagationSource: messageOrArray.propagationSource, + acceptance: acceptanceArr[0], + }); + }, 0); + } + } + + private trackJobTime(message: PendingGossipsubMessage, numJob: number): void { if (message.startProcessUnixSec !== null) { this.metrics?.gossipValidationQueue.jobWaitTime.observe( {topic: message.topic.type}, message.startProcessUnixSec - message.seenTimestampSec ); + // if it takes 64ms to process 64 jobs, the average job time is 1ms this.metrics?.gossipValidationQueue.jobTime.observe( {topic: message.topic.type}, - Date.now() / 1000 - message.startProcessUnixSec + (Date.now() / 1000 - message.startProcessUnixSec) / numJob ); } - - // Use setTimeout to yield to the macro queue - // This is mostly due to too many attestation messages, and a gossipsub RPC may - // contain multiple of them. This helps avoid the I/O lag issue. - setTimeout( - () => - this.events.emit(NetworkEvent.gossipMessageValidationResult, { - msgId: message.msgId, - propagationSource: message.propagationSource, - acceptance, - }), - 0 - ); } /** diff --git a/packages/beacon-node/src/network/processor/types.ts b/packages/beacon-node/src/network/processor/types.ts index fcb3fd90b366..c571e7ab4870 100644 --- a/packages/beacon-node/src/network/processor/types.ts +++ b/packages/beacon-node/src/network/processor/types.ts @@ -12,6 +12,8 @@ export type PendingGossipsubMessage = { msg: Message; // only available for beacon_attestation and aggregate_and_proof msgSlot?: Slot; + // indexed data if any, only available for beacon_attestation as a result of getAttDataBase64FromAttestationSerialized + indexed?: string; msgId: string; // TODO: Refactor into accepting string (requires gossipsub changes) for easier multi-threading propagationSource: PeerIdStr; diff --git a/packages/beacon-node/src/util/wrapError.ts b/packages/beacon-node/src/util/wrapError.ts index f64661b926a8..3b25da203c47 100644 --- a/packages/beacon-node/src/util/wrapError.ts +++ b/packages/beacon-node/src/util/wrapError.ts @@ -1,4 +1,4 @@ -type Result = {err: null; result: T} | {err: Error}; +export type Result = {err: null; result: T} | {err: Error}; /** * Wraps a promise to return either an error or result diff --git a/packages/beacon-node/test/e2e/network/gossipsub.test.ts b/packages/beacon-node/test/e2e/network/gossipsub.test.ts index b943c356ff00..49ba6c42d90e 100644 --- a/packages/beacon-node/test/e2e/network/gossipsub.test.ts +++ b/packages/beacon-node/test/e2e/network/gossipsub.test.ts @@ -4,7 +4,7 @@ import {sleep} from "@lodestar/utils"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {ssz} from "@lodestar/types"; import {Network} from "../../../src/network/index.js"; -import {GossipType, GossipHandlers} from "../../../src/network/gossip/index.js"; +import {GossipType, GossipHandlers, GossipHandlerParamGeneric} from "../../../src/network/gossip/index.js"; import {connect, onPeerConnect, getNetworkForTest} from "../../utils/network.js"; describe("gossipsub / main thread", function () { @@ -57,8 +57,8 @@ function runTests(this: Mocha.Suite, {useWorker}: {useWorker: boolean}): void { const onVoluntaryExitPromise = new Promise((resolve) => (onVoluntaryExit = resolve)); const {netA, netB} = await mockModules({ - [GossipType.voluntary_exit]: async ({serializedData}) => { - onVoluntaryExit(serializedData); + [GossipType.voluntary_exit]: async ({gossipData}: GossipHandlerParamGeneric) => { + onVoluntaryExit(gossipData.serializedData); }, }); @@ -90,8 +90,10 @@ function runTests(this: Mocha.Suite, {useWorker}: {useWorker: boolean}): void { const onBlsToExecutionChangePromise = new Promise((resolve) => (onBlsToExecutionChange = resolve)); const {netA, netB} = await mockModules({ - [GossipType.bls_to_execution_change]: async ({serializedData}) => { - onBlsToExecutionChange(serializedData); + [GossipType.bls_to_execution_change]: async ({ + gossipData, + }: GossipHandlerParamGeneric) => { + onBlsToExecutionChange(gossipData.serializedData); }, }); @@ -124,8 +126,10 @@ function runTests(this: Mocha.Suite, {useWorker}: {useWorker: boolean}): void { ); const {netA, netB} = await mockModules({ - [GossipType.light_client_optimistic_update]: async ({serializedData}) => { - onLightClientOptimisticUpdate(serializedData); + [GossipType.light_client_optimistic_update]: async ({ + gossipData, + }: GossipHandlerParamGeneric) => { + onLightClientOptimisticUpdate(gossipData.serializedData); }, }); @@ -161,8 +165,10 @@ function runTests(this: Mocha.Suite, {useWorker}: {useWorker: boolean}): void { ); const {netA, netB} = await mockModules({ - [GossipType.light_client_finality_update]: async ({serializedData}) => { - onLightClientFinalityUpdate(serializedData); + [GossipType.light_client_finality_update]: async ({ + gossipData, + }: GossipHandlerParamGeneric) => { + onLightClientFinalityUpdate(gossipData.serializedData); }, }); diff --git a/packages/beacon-node/test/perf/bls/bls.test.ts b/packages/beacon-node/test/perf/bls/bls.test.ts index c73723c22d94..bd6a26a3f692 100644 --- a/packages/beacon-node/test/perf/bls/bls.test.ts +++ b/packages/beacon-node/test/perf/bls/bls.test.ts @@ -1,20 +1,26 @@ +import crypto from "node:crypto"; import {itBench} from "@dapplion/benchmark"; import bls from "@chainsafe/bls"; -import type {PublicKey, SecretKey, Signature} from "@chainsafe/bls/types"; +import {CoordType, type PublicKey, type SecretKey} from "@chainsafe/bls/types"; import {linspace} from "../../../src/util/numpy.js"; describe("BLS ops", function () { type Keypair = {publicKey: PublicKey; secretKey: SecretKey}; - type BlsSet = {publicKey: PublicKey; message: Uint8Array; signature: Signature}; + // signature needs to be in Uint8Array to match real situation + type BlsSet = {publicKey: PublicKey; message: Uint8Array; signature: Uint8Array}; // Create and cache (on demand) crypto data to benchmark const sets = new Map(); + const sameMessageSets = new Map(); const keypairs = new Map(); function getKeypair(i: number): Keypair { let keypair = keypairs.get(i); if (!keypair) { - const secretKey = bls.SecretKey.fromBytes(Buffer.alloc(32, i + 1)); + const bytes = new Uint8Array(32); + const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + dataView.setUint32(0, i + 1, true); + const secretKey = bls.SecretKey.fromBytes(bytes); const publicKey = secretKey.toPublicKey(); keypair = {secretKey, publicKey}; keypairs.set(i, keypair); @@ -27,26 +33,64 @@ describe("BLS ops", function () { if (!set) { const {secretKey, publicKey} = getKeypair(i); const message = Buffer.alloc(32, i + 1); - set = {publicKey, message: message, signature: secretKey.sign(message)}; + set = {publicKey, message: message, signature: secretKey.sign(message).toBytes()}; sets.set(i, set); } return set; } + const seedMessage = crypto.randomBytes(32); + function getSetSameMessage(i: number): BlsSet { + const message = new Uint8Array(32); + message.set(seedMessage); + let set = sameMessageSets.get(i); + if (!set) { + const {secretKey, publicKey} = getKeypair(i); + set = {publicKey, message, signature: secretKey.sign(message).toBytes()}; + sameMessageSets.set(i, set); + } + return set; + } + // Note: getSet() caches the value, does not re-compute every time itBench({id: `BLS verify - ${bls.implementation}`, beforeEach: () => getSet(0)}, (set) => { - const isValid = set.signature.verify(set.publicKey, set.message); + const isValid = bls.Signature.fromBytes(set.signature).verify(set.publicKey, set.message); if (!isValid) throw Error("Invalid"); }); // An aggregate and proof object has 3 signatures. // We may want to bundle up to 32 sets in a single batch. - for (const count of [3, 8, 32]) { + for (const count of [3, 8, 32, 64, 128]) { itBench({ id: `BLS verifyMultipleSignatures ${count} - ${bls.implementation}`, beforeEach: () => linspace(0, count - 1).map((i) => getSet(i)), fn: (sets) => { - const isValid = bls.Signature.verifyMultipleSignatures(sets); + const isValid = bls.Signature.verifyMultipleSignatures( + sets.map((set) => ({ + publicKey: set.publicKey, + message: set.message, + signature: bls.Signature.fromBytes(set.signature), + })) + ); + if (!isValid) throw Error("Invalid"); + }, + }); + } + + // An aggregate and proof object has 3 signatures. + // We may want to bundle up to 32 sets in a single batch. + // TODO: figure out why it does not work with 256 or more + for (const count of [3, 8, 32, 64, 128]) { + itBench({ + id: `BLS verifyMultipleSignatures - same message - ${count} - ${bls.implementation}`, + beforeEach: () => linspace(0, count - 1).map((i) => getSetSameMessage(i)), + fn: (sets) => { + // aggregate and verify aggregated signatures + const aggregatedPubkey = bls.PublicKey.aggregate(sets.map((set) => set.publicKey)); + const aggregatedSignature = bls.Signature.aggregate( + sets.map((set) => bls.Signature.fromBytes(set.signature, CoordType.affine, false)) + ); + const isValid = aggregatedSignature.verify(aggregatedPubkey, sets[0].message); if (!isValid) throw Error("Invalid"); }, }); diff --git a/packages/beacon-node/test/perf/chain/validation/attestation.test.ts b/packages/beacon-node/test/perf/chain/validation/attestation.test.ts index 84b423dd7e62..f0a4f7c3e89d 100644 --- a/packages/beacon-node/test/perf/chain/validation/attestation.test.ts +++ b/packages/beacon-node/test/perf/chain/validation/attestation.test.ts @@ -1,40 +1,83 @@ -import {itBench} from "@dapplion/benchmark"; -import {ssz} from "@lodestar/types"; // eslint-disable-next-line import/no-relative-packages -import {generateTestCachedBeaconStateOnlyValidators} from "../../../../../state-transition/test/perf/util.js"; -import {validateApiAttestation, validateGossipAttestation} from "../../../../src/chain/validation/index.js"; +import {itBench, setBenchOpts} from "@dapplion/benchmark"; +import {expect} from "chai"; +import {ssz} from "@lodestar/types"; +import {generateTestCachedBeaconStateOnlyValidators} from "@lodestar/state-transition/test/perf/util.js"; +import {validateAttestation, validateGossipAttestationsSameAttData} from "../../../../src/chain/validation/index.js"; import {getAttestationValidData} from "../../../utils/validationData/attestation.js"; +import {getAttDataBase64FromAttestationSerialized} from "../../../../src/util/sszBytes.js"; -describe("validate attestation", () => { - const vc = 64; +describe("validate gossip attestation", () => { + setBenchOpts({ + minMs: 30_000, + }); + + const vc = 640_000; const stateSlot = 100; + const state = generateTestCachedBeaconStateOnlyValidators({vc, slot: stateSlot}); - const {chain, attestation, subnet} = getAttestationValidData({ + const { + chain, + attestation: attestation0, + subnet: subnet0, + } = getAttestationValidData({ currentSlot: stateSlot, - state: generateTestCachedBeaconStateOnlyValidators({vc, slot: stateSlot}), + state, + bitIndex: 0, + // enable this in local environment to match production + // blsVerifyAllMainThread: false, + }); + + const attSlot = attestation0.data.slot; + const serializedData = ssz.phase0.Attestation.serialize(attestation0); + const fork = chain.config.getForkName(stateSlot); + itBench({ + id: `validate gossip attestation - vc ${vc}`, + beforeEach: () => chain.seenAttesters["validatorIndexesByEpoch"].clear(), + fn: async () => { + await validateAttestation( + fork, + chain, + { + attestation: null, + serializedData, + attSlot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, + subnet0 + ); + }, }); - const attStruct = attestation; + for (const chunkSize of [32, 64, 128, 256]) { + const attestations = [attestation0]; + for (let i = 1; i < chunkSize; i++) { + const {attestation, subnet} = getAttestationValidData({ + currentSlot: stateSlot, + state, + bitIndex: i, + }); + expect(subnet).to.be.equal(subnet0); + attestations.push(attestation); + } - for (const [id, att] of Object.entries({struct: attStruct})) { - const serializedData = ssz.phase0.Attestation.serialize(att); - const slot = attestation.data.slot; - itBench({ - id: `validate api attestation - ${id}`, - beforeEach: () => chain.seenAttesters["validatorIndexesByEpoch"].clear(), - fn: async () => { - const fork = chain.config.getForkName(stateSlot); - await validateApiAttestation(fork, chain, {attestation: att, serializedData: null}); - }, + const attestationOrBytesArr = attestations.map((att) => { + const serializedData = ssz.phase0.Attestation.serialize(att); + return { + attestation: null, + serializedData, + attSlot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }; }); itBench({ - id: `validate gossip attestation - ${id}`, + id: `batch validate gossip attestation - vc ${vc} - chunk ${chunkSize}`, beforeEach: () => chain.seenAttesters["validatorIndexesByEpoch"].clear(), fn: async () => { - const fork = chain.config.getForkName(stateSlot); - await validateGossipAttestation(fork, chain, {attestation: null, serializedData, attSlot: slot}, subnet); + await validateGossipAttestationsSameAttData(fork, chain, attestationOrBytesArr, subnet0); }, + runsFactor: chunkSize, }); } }); diff --git a/packages/beacon-node/test/unit/chain/validation/attestation.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation.test.ts index f28f62abc229..24dc073891e5 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation.test.ts @@ -1,21 +1,30 @@ import sinon, {SinonStubbedInstance} from "sinon"; import {expect} from "chai"; import {BitArray} from "@chainsafe/ssz"; -import {SLOTS_PER_EPOCH} from "@lodestar/params"; -import {computeEpochAtSlot, computeStartSlotAtEpoch, processSlots} from "@lodestar/state-transition"; +import type {PublicKey, SecretKey} from "@chainsafe/bls/types"; +import bls from "@chainsafe/bls"; +import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; import {defaultChainConfig, createChainForkConfig, BeaconConfig} from "@lodestar/config"; -import {Slot, ssz} from "@lodestar/types"; import {ProtoBlock} from "@lodestar/fork-choice"; // eslint-disable-next-line import/no-relative-packages -import {generateTestCachedBeaconStateOnlyValidators} from "../../../../../state-transition/test/perf/util.js"; +import {SignatureSetType, computeEpochAtSlot, computeStartSlotAtEpoch, processSlots} from "@lodestar/state-transition"; +import {Slot, ssz} from "@lodestar/types"; +import {generateTestCachedBeaconStateOnlyValidators} from "@lodestar/state-transition/test/perf/util.js"; import {IBeaconChain} from "../../../../src/chain/index.js"; -import {AttestationErrorCode, GossipErrorCode} from "../../../../src/chain/errors/index.js"; +import { + AttestationError, + AttestationErrorCode, + GossipAction, + GossipErrorCode, +} from "../../../../src/chain/errors/index.js"; import { ApiAttestation, GossipAttestation, getStateForAttestationVerification, validateApiAttestation, - validateGossipAttestation, + Phase0Result, + validateAttestation, + validateGossipAttestationsSameAttData, } from "../../../../src/chain/validation/index.js"; import {expectRejectedWithLodestarError} from "../../../utils/errors.js"; import {memoOnce} from "../../../utils/cache.js"; @@ -25,7 +34,120 @@ import {StateRegenerator} from "../../../../src/chain/regen/regen.js"; import {ZERO_HASH_HEX} from "../../../../src/constants/constants.js"; import {QueuedStateRegenerator} from "../../../../src/chain/regen/queued.js"; -describe("chain / validation / attestation", () => { +import {BlsSingleThreadVerifier} from "../../../../src/chain/bls/singleThread.js"; +import {SeenAttesters} from "../../../../src/chain/seenCache/seenAttesters.js"; +import {getAttDataBase64FromAttestationSerialized} from "../../../../src/util/sszBytes.js"; + +describe("validateGossipAttestationsSameAttData", () => { + // phase0Result specifies whether the attestation is valid in phase0 + // phase1Result specifies signature verification + const testCases: {phase0Result: boolean[]; phase1Result: boolean[]; seenAttesters: number[]}[] = [ + { + phase0Result: [true, true, true, true, true], + phase1Result: [true, true, true, true, true], + seenAttesters: [0, 1, 2, 3, 4], + }, + { + phase0Result: [false, true, true, true, true], + phase1Result: [true, false, true, true, true], + seenAttesters: [2, 3, 4], + }, + { + phase0Result: [false, false, true, true, true], + phase1Result: [true, false, false, true, true], + seenAttesters: [3, 4], + }, + { + phase0Result: [false, false, true, true, true], + phase1Result: [true, false, false, true, false], + seenAttesters: [3], + }, + { + phase0Result: [false, false, true, true, true], + phase1Result: [true, true, false, false, false], + seenAttesters: [], + }, + ]; + + type Keypair = {publicKey: PublicKey; secretKey: SecretKey}; + const keypairs = new Map(); + function getKeypair(i: number): Keypair { + let keypair = keypairs.get(i); + if (!keypair) { + const bytes = new Uint8Array(32); + const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + dataView.setUint32(0, i + 1, true); + const secretKey = bls.SecretKey.fromBytes(bytes); + const publicKey = secretKey.toPublicKey(); + keypair = {secretKey, publicKey}; + keypairs.set(i, keypair); + } + return keypair; + } + + let chain: IBeaconChain; + const signingRoot = Buffer.alloc(32, 1); + + beforeEach(() => { + chain = { + bls: new BlsSingleThreadVerifier({metrics: null}), + seenAttesters: new SeenAttesters(), + } as Partial as IBeaconChain; + }); + + for (const [testCaseIndex, testCase] of testCases.entries()) { + const {phase0Result, phase1Result, seenAttesters} = testCase; + it(`test case ${testCaseIndex}`, async () => { + const phase0Results: Promise[] = []; + for (const [i, isValid] of phase0Result.entries()) { + const signatureSet = { + type: SignatureSetType.single, + pubkey: getKeypair(i).publicKey, + signingRoot, + signature: getKeypair(i).secretKey.sign(signingRoot).toBytes(), + }; + if (isValid) { + if (!phase1Result[i]) { + // invalid signature + signatureSet.signature = getKeypair(2023).secretKey.sign(signingRoot).toBytes(); + } + phase0Results.push( + Promise.resolve({ + attestation: ssz.phase0.Attestation.defaultValue(), + signatureSet, + validatorIndex: i, + } as Partial as Phase0Result) + ); + } else { + phase0Results.push( + Promise.reject( + new AttestationError(GossipAction.REJECT, { + code: AttestationErrorCode.BAD_TARGET_EPOCH, + }) + ) + ); + } + } + + let callIndex = 0; + const phase0ValidationFn = (): Promise => { + const result = phase0Results[callIndex]; + callIndex++; + return result; + }; + await validateGossipAttestationsSameAttData(ForkName.phase0, chain, new Array(5).fill({}), 0, phase0ValidationFn); + for (let validatorIndex = 0; validatorIndex < phase0Result.length; validatorIndex++) { + if (seenAttesters.includes(validatorIndex)) { + expect(chain.seenAttesters.isKnown(0, validatorIndex)).to.be.true; + } else { + expect(chain.seenAttesters.isKnown(0, validatorIndex)).to.be.false; + } + } + }); // end test case + } +}); + +describe("validateAttestation", () => { const vc = 64; const stateSlot = 100; @@ -60,7 +182,7 @@ describe("chain / validation / attestation", () => { const {chain, subnet} = getValidData(); await expectGossipError( chain, - {attestation: null, serializedData: Buffer.alloc(0), attSlot: 0}, + {attestation: null, serializedData: Buffer.alloc(0), attSlot: 0, attDataBase64: "invalid"}, subnet, GossipErrorCode.INVALID_SERIALIZED_BYTES_ERROR_CODE ); @@ -76,7 +198,12 @@ describe("chain / validation / attestation", () => { await expectApiError(chain, {attestation, serializedData: null}, AttestationErrorCode.BAD_TARGET_EPOCH); await expectGossipError( chain, - {attestation: null, serializedData, attSlot: attestation.data.slot}, + { + attestation: null, + serializedData, + attSlot: attestation.data.slot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, subnet, AttestationErrorCode.BAD_TARGET_EPOCH ); @@ -90,7 +217,12 @@ describe("chain / validation / attestation", () => { await expectApiError(chain, {attestation, serializedData: null}, AttestationErrorCode.PAST_SLOT); await expectGossipError( chain, - {attestation: null, serializedData, attSlot: attestation.data.slot}, + { + attestation: null, + serializedData, + attSlot: attestation.data.slot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, subnet, AttestationErrorCode.PAST_SLOT ); @@ -104,7 +236,12 @@ describe("chain / validation / attestation", () => { await expectApiError(chain, {attestation, serializedData: null}, AttestationErrorCode.FUTURE_SLOT); await expectGossipError( chain, - {attestation: null, serializedData, attSlot: attestation.data.slot}, + { + attestation: null, + serializedData, + attSlot: attestation.data.slot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, subnet, AttestationErrorCode.FUTURE_SLOT ); @@ -124,7 +261,12 @@ describe("chain / validation / attestation", () => { ); await expectGossipError( chain, - {attestation: null, serializedData, attSlot: attestation.data.slot}, + { + attestation: null, + serializedData, + attSlot: attestation.data.slot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, subnet, AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET ); @@ -139,7 +281,12 @@ describe("chain / validation / attestation", () => { await expectGossipError( chain, - {attestation: null, serializedData, attSlot: attestation.data.slot}, + { + attestation: null, + serializedData, + attSlot: attestation.data.slot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, subnet, AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET ); @@ -158,7 +305,12 @@ describe("chain / validation / attestation", () => { ); await expectGossipError( chain, - {attestation: null, serializedData, attSlot: attestation.data.slot}, + { + attestation: null, + serializedData, + attSlot: attestation.data.slot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, subnet, AttestationErrorCode.UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT ); @@ -173,7 +325,12 @@ describe("chain / validation / attestation", () => { await expectApiError(chain, {attestation, serializedData: null}, AttestationErrorCode.INVALID_TARGET_ROOT); await expectGossipError( chain, - {attestation: null, serializedData, attSlot: attestation.data.slot}, + { + attestation: null, + serializedData, + attSlot: attestation.data.slot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, subnet, AttestationErrorCode.INVALID_TARGET_ROOT ); @@ -197,7 +354,12 @@ describe("chain / validation / attestation", () => { ); await expectGossipError( chain, - {attestation: null, serializedData, attSlot: attestation.data.slot}, + { + attestation: null, + serializedData, + attSlot: attestation.data.slot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, subnet, AttestationErrorCode.NO_COMMITTEE_FOR_SLOT_AND_INDEX ); @@ -219,7 +381,12 @@ describe("chain / validation / attestation", () => { ); await expectGossipError( chain, - {attestation: null, serializedData, attSlot: attestation.data.slot}, + { + attestation: null, + serializedData, + attSlot: attestation.data.slot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, subnet, AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS ); @@ -233,7 +400,12 @@ describe("chain / validation / attestation", () => { await expectGossipError( chain, - {attestation: null, serializedData, attSlot: attestation.data.slot}, + { + attestation: null, + serializedData, + attSlot: attestation.data.slot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, invalidSubnet, AttestationErrorCode.INVALID_SUBNET_ID ); @@ -248,7 +420,12 @@ describe("chain / validation / attestation", () => { await expectApiError(chain, {attestation, serializedData: null}, AttestationErrorCode.ATTESTATION_ALREADY_KNOWN); await expectGossipError( chain, - {attestation: null, serializedData, attSlot: attestation.data.slot}, + { + attestation: null, + serializedData, + attSlot: attestation.data.slot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, subnet, AttestationErrorCode.ATTESTATION_ALREADY_KNOWN ); @@ -265,7 +442,12 @@ describe("chain / validation / attestation", () => { await expectApiError(chain, {attestation, serializedData: null}, AttestationErrorCode.INVALID_SIGNATURE); await expectGossipError( chain, - {attestation: null, serializedData, attSlot: attestation.data.slot}, + { + attestation: null, + serializedData, + attSlot: attestation.data.slot, + attDataBase64: getAttDataBase64FromAttestationSerialized(serializedData), + }, subnet, AttestationErrorCode.INVALID_SIGNATURE ); @@ -288,10 +470,7 @@ describe("chain / validation / attestation", () => { errorCode: string ): Promise { const fork = chain.config.getForkName(stateSlot); - await expectRejectedWithLodestarError( - validateGossipAttestation(fork, chain, attestationOrBytes, subnet), - errorCode - ); + await expectRejectedWithLodestarError(validateAttestation(fork, chain, attestationOrBytes, subnet), errorCode); } }); diff --git a/packages/beacon-node/test/utils/validationData/attestation.ts b/packages/beacon-node/test/utils/validationData/attestation.ts index 314d3d255b62..6f768227e5cd 100644 --- a/packages/beacon-node/test/utils/validationData/attestation.ts +++ b/packages/beacon-node/test/utils/validationData/attestation.ts @@ -14,12 +14,13 @@ import {IBeaconChain} from "../../../src/chain/index.js"; import {IStateRegenerator} from "../../../src/chain/regen/index.js"; import {ZERO_HASH, ZERO_HASH_HEX} from "../../../src/constants/index.js"; import {SeenAttesters} from "../../../src/chain/seenCache/index.js"; -import {BlsSingleThreadVerifier} from "../../../src/chain/bls/index.js"; +import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier} from "../../../src/chain/bls/index.js"; import {signCached} from "../cache.js"; import {ClockStatic} from "../clock.js"; import {SeenAggregatedAttestations} from "../../../src/chain/seenCache/seenAggregateAndProof.js"; import {SeenAttestationDatas} from "../../../src/chain/seenCache/seenAttestationData.js"; import {defaultChainOptions} from "../../../src/chain/options.js"; +import {testLogger} from "../logger.js"; export type AttestationValidDataOpts = { currentSlot?: Slot; @@ -28,6 +29,7 @@ export type AttestationValidDataOpts = { bitIndex?: number; targetRoot?: Uint8Array; beaconBlockRoot?: Uint8Array; + blsVerifyAllMainThread?: boolean; state: ReturnType; }; @@ -46,6 +48,7 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { const bitIndex = opts.bitIndex ?? 0; const targetRoot = opts.targetRoot ?? ZERO_HASH; const beaconBlockRoot = opts.beaconBlockRoot ?? ZERO_HASH; + const blsVerifyAllMainThread = opts.blsVerifyAllMainThread ?? true; // Create cached state const state = opts.state; @@ -124,7 +127,9 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { seenAttesters: new SeenAttesters(), seenAggregatedAttestations: new SeenAggregatedAttestations(null), seenAttestationDatas: new SeenAttestationDatas(null, 0, 0), - bls: new BlsSingleThreadVerifier({metrics: null}), + bls: blsVerifyAllMainThread + ? new BlsSingleThreadVerifier({metrics: null}) + : new BlsMultiThreadWorkerPool({}, {logger: testLogger(), metrics: null}), waitForBlock: () => Promise.resolve(false), index2pubkey: state.epochCtx.index2pubkey, opts: defaultChainOptions, diff --git a/packages/state-transition/src/util/signatureSets.ts b/packages/state-transition/src/util/signatureSets.ts index 770f66e7ecd9..3aa3480cba88 100644 --- a/packages/state-transition/src/util/signatureSets.ts +++ b/packages/state-transition/src/util/signatureSets.ts @@ -7,19 +7,21 @@ export enum SignatureSetType { aggregate = "aggregate", } -export type ISignatureSet = - | { - type: SignatureSetType.single; - pubkey: PublicKey; - signingRoot: Root; - signature: Uint8Array; - } - | { - type: SignatureSetType.aggregate; - pubkeys: PublicKey[]; - signingRoot: Root; - signature: Uint8Array; - }; +export type SingleSignatureSet = { + type: SignatureSetType.single; + pubkey: PublicKey; + signingRoot: Root; + signature: Uint8Array; +}; + +export type AggregatedSignatureSet = { + type: SignatureSetType.aggregate; + pubkeys: PublicKey[]; + signingRoot: Root; + signature: Uint8Array; +}; + +export type ISignatureSet = SingleSignatureSet | AggregatedSignatureSet; export function verifySignatureSet(signatureSet: ISignatureSet): boolean { // All signatures are not trusted and must be group checked (p2.subgroup_check) @@ -41,7 +43,7 @@ export function createSingleSignatureSetFromComponents( pubkey: PublicKey, signingRoot: Root, signature: Uint8Array -): ISignatureSet { +): SingleSignatureSet { return { type: SignatureSetType.single, pubkey, @@ -54,7 +56,7 @@ export function createAggregateSignatureSetFromComponents( pubkeys: PublicKey[], signingRoot: Root, signature: Uint8Array -): ISignatureSet { +): AggregatedSignatureSet { return { type: SignatureSetType.aggregate, pubkeys, diff --git a/packages/state-transition/test/perf/util.ts b/packages/state-transition/test/perf/util.ts index 316860fef447..169b205ce5c6 100644 --- a/packages/state-transition/test/perf/util.ts +++ b/packages/state-transition/test/perf/util.ts @@ -424,9 +424,13 @@ export function generateTestCachedBeaconStateOnlyValidators({ throw Error(`Wrong number of validators in the state: ${state.validators.length} !== ${vc}`); } - return createCachedBeaconState(state, { - config: createBeaconConfig(config, state.genesisValidatorsRoot), - pubkey2index, - index2pubkey, - }); + return createCachedBeaconState( + state, + { + config: createBeaconConfig(config, state.genesisValidatorsRoot), + pubkey2index, + index2pubkey, + }, + {skipSyncPubkeys: true} + ); }