UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

214 lines • 13.8 kB
import { ForkSeq } from "@lodestar/params"; import { computeEpochAtSlot, createAggregateSignatureSetFromComponents, isAggregatorFromCommitteeLength, } from "@lodestar/state-transition"; import { ssz } from "@lodestar/types"; import { toRootHex } from "@lodestar/utils"; import { AttestationError, AttestationErrorCode, GossipAction } from "../errors/index.js"; import { RegenCaller } from "../regen/index.js"; import { getAttestationDataSigningRoot, getCommitteeValidatorIndices, getSeenAttDataKeyFromSignedAggregateAndProof, getShufflingForAttestationVerification, verifyHeadBlockAndTargetRoot, verifyPropagationSlotRange, } from "./attestation.js"; import { getAggregateAndProofSignatureSet, getSelectionProofSignatureSet } from "./signatureSets/index.js"; export async function validateApiAggregateAndProof(fork, chain, signedAggregateAndProof) { const skipValidationKnownAttesters = true; const prioritizeBls = true; return validateAggregateAndProof(fork, chain, signedAggregateAndProof, null, { skipValidationKnownAttesters, prioritizeBls, }); } export async function validateGossipAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData) { return validateAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData); } async function validateAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData = null, opts = { skipValidationKnownAttesters: false, prioritizeBls: false, }) { const { skipValidationKnownAttesters, prioritizeBls } = opts; // Do checks in this order: // - do early checks (w/o indexed attestation) // - > obtain indexed attestation and committes per slot // - do middle checks w/ indexed attestation // - > verify signature // - do late checks w/ a valid signature const aggregateAndProof = signedAggregateAndProof.message; const aggregate = aggregateAndProof.aggregate; const { aggregationBits } = aggregate; const attData = aggregate.data; const attSlot = attData.slot; let committeeIndex; if (ForkSeq[fork] >= ForkSeq.gloas) { // [REJECT] `aggregate.data.index < 2`. if (attData.index >= 2) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.INVALID_PAYLOAD_STATUS_VALUE, attDataIndex: attData.index, }); } // [REJECT] `aggregate.data.index == 0` if `block.slot == aggregate.data.slot`. const block = chain.forkChoice.getBlockDefaultStatus(attData.beaconBlockRoot); // If block is unknown, we don't handle it here. It will throw error later on at `verifyHeadBlockAndTargetRoot()` if (block !== null && block.slot === attData.slot && attData.index !== 0) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.PREMATURELY_INDICATED_PAYLOAD_PRESENT, }); } // [REJECT] If `aggregate.data.index == 1` (payload present for a past // block), the execution payload for `block` passes validation. // [IGNORE] When `aggregate.data.index == 1` (payload present for a past block), // the corresponding execution payload for `block` has been seen (a client MAY queue // attestations for processing once the payload is retrieved and SHOULD request the // payload envelope via `ExecutionPayloadEnvelopesByRoot`). if (block !== null && attData.index === 1 && !chain.seenPayloadEnvelope(toRootHex(attData.beaconBlockRoot))) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.EXECUTION_PAYLOAD_NOT_SEEN, beaconBlockRoot: toRootHex(attData.beaconBlockRoot), }); } // [REJECT] len(committee_indices) == 1, where committee_indices = get_committee_indices(aggregate) committeeIndex = aggregate.committeeBits.getSingleTrueBit(); if (committeeIndex === null) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET }); } } else if (ForkSeq[fork] >= ForkSeq.electra) { committeeIndex = aggregate.committeeBits.getSingleTrueBit(); // [REJECT] len(committee_indices) == 1, where committee_indices = get_committee_indices(aggregate) if (committeeIndex === null) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET }); } // [REJECT] aggregate.data.index == 0 if (attData.index !== 0) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX }); } } else { committeeIndex = attData.index; } const seenAttDataKey = serializedData ? getSeenAttDataKeyFromSignedAggregateAndProof(fork, serializedData) : null; const cachedAttData = seenAttDataKey ? chain.seenAttestationDatas.get(attSlot, committeeIndex, seenAttDataKey) : null; const attEpoch = computeEpochAtSlot(attSlot); const attTarget = attData.target; const targetEpoch = attTarget.epoch; chain.metrics?.gossipAttestation.attestationSlotToClockSlot.observe({ caller: RegenCaller.validateGossipAggregateAndProof }, chain.clock.currentSlot - attSlot); if (!cachedAttData) { // [REJECT] The attestation's epoch matches its target -- i.e. attestation.data.target.epoch == compute_epoch_at_slot(attestation.data.slot) if (targetEpoch !== attEpoch) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.BAD_TARGET_EPOCH }); } // Pre-deneb: // [IGNORE] aggregate.data.slot is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) // -- i.e. aggregate.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= aggregate.data.slot // (a client MAY queue future aggregates for processing at the appropriate slot). // Post-deneb: // [IGNORE] `aggregate.data.slot` is equal to or earlier than the `current_slot` (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) // -- i.e. `aggregate.data.slot <= current_slot` // (a client MAY queue future aggregates for processing at the appropriate slot). // [IGNORE] the epoch of `aggregate.data.slot` is either the current or previous epoch // (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) // -- i.e. `compute_epoch_at_slot(aggregate.data.slot) in (get_previous_epoch(state), get_current_epoch(state))` verifyPropagationSlotRange(fork, chain, attSlot); } // [IGNORE] The aggregate is the first valid aggregate received for the aggregator with // index aggregate_and_proof.aggregator_index for the epoch aggregate.data.target.epoch. const aggregatorIndex = aggregateAndProof.aggregatorIndex; if (chain.seenAggregators.isKnown(targetEpoch, aggregatorIndex)) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.AGGREGATOR_ALREADY_KNOWN, targetEpoch, aggregatorIndex, }); } // _[IGNORE]_ A valid aggregate attestation defined by `hash_tree_root(aggregate.data)` whose `aggregation_bits` // is a non-strict superset has _not_ already been seen. const attDataRootHex = cachedAttData ? cachedAttData.attDataRootHex : toRootHex(ssz.phase0.AttestationData.hashTreeRoot(attData)); if (!skipValidationKnownAttesters && chain.seenAggregatedAttestations.isKnown(targetEpoch, committeeIndex, attDataRootHex, aggregationBits)) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.ATTESTERS_ALREADY_KNOWN, targetEpoch, aggregateRoot: attDataRootHex, }); } // [IGNORE] The block being voted for (attestation.data.beacon_block_root) has been seen (via both gossip // and non-gossip sources) (a client MAY queue attestations for processing once block is retrieved). // Lighthouse doesn't check maxSkipSlots option here but Lodestar wants to be more strict // to be more DOS protection // [REJECT] The aggregate attestation's target block is an ancestor of the block named in the LMD vote // -- i.e. `get_checkpoint_block(store, aggregate.data.beacon_block_root, aggregate.data.target.epoch) == aggregate.data.target.root` const attHeadBlock = verifyHeadBlockAndTargetRoot(chain, attData.beaconBlockRoot, attTarget.root, attSlot, attEpoch, RegenCaller.validateGossipAggregateAndProof, chain.opts.maxSkipSlots); // [IGNORE] The current finalized_checkpoint is an ancestor of the block defined by aggregate.data.beacon_block_root // -- i.e. get_ancestor(store, aggregate.data.beacon_block_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == store.finalized_checkpoint.root // > Altready check in `chain.forkChoice.hasBlock(attestation.data.beaconBlockRoot)` const shuffling = await getShufflingForAttestationVerification(chain, attEpoch, attHeadBlock, RegenCaller.validateGossipAttestation); // [REJECT] The committee index is within the expected range // -- i.e. data.index < get_committee_count_per_slot(state, data.target.epoch) const committeeValidatorIndices = cachedAttData ? cachedAttData.committeeValidatorIndices : getCommitteeValidatorIndices(shuffling, attSlot, committeeIndex); // [REJECT] The number of aggregation bits matches the committee size // -- i.e. `len(aggregation_bits) == len(get_beacon_committee(state, aggregate.data.slot, index))`. if (aggregate.aggregationBits.bitLen !== committeeValidatorIndices.length) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS }); } const attestingIndices = aggregate.aggregationBits.intersectValues(committeeValidatorIndices); const indexedAttestation = { attestingIndices, data: attData, signature: aggregate.signature, }; // TODO: Check this before regen // [REJECT] The attestation has participants -- that is, // len(get_attesting_indices(state, aggregate.data, aggregate.aggregation_bits)) >= 1. if (attestingIndices.length < 1) { // missing attestation participants throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.EMPTY_AGGREGATION_BITFIELD }); } // [REJECT] aggregate_and_proof.selection_proof selects the validator as an aggregator for the slot // -- i.e. is_aggregator(state, aggregate.data.slot, aggregate.data.index, aggregate_and_proof.selection_proof) returns True. if (!isAggregatorFromCommitteeLength(committeeValidatorIndices.length, aggregateAndProof.selectionProof)) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.INVALID_AGGREGATOR }); } // [REJECT] The aggregator's validator index is within the committee // -- i.e. aggregate_and_proof.aggregator_index in get_beacon_committee(state, aggregate.data.slot, aggregate.data.index). if (!committeeValidatorIndices.includes(aggregateAndProof.aggregatorIndex)) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.AGGREGATOR_NOT_IN_COMMITTEE }); } // [REJECT] The aggregate_and_proof.selection_proof is a valid signature of the aggregate.data.slot // by the validator with index aggregate_and_proof.aggregator_index. // [REJECT] The aggregator signature, signed_aggregate_and_proof.signature, is valid. // [REJECT] The signature of aggregate is valid. const signingRoot = cachedAttData ? cachedAttData.signingRoot : getAttestationDataSigningRoot(chain.config, attData); const indexedAttestationSignatureSet = createAggregateSignatureSetFromComponents(indexedAttestation.attestingIndices, signingRoot, indexedAttestation.signature); const signatureSets = [ getSelectionProofSignatureSet(chain.config, attSlot, aggregatorIndex, signedAggregateAndProof), getAggregateAndProofSignatureSet(chain.config, attEpoch, aggregatorIndex, signedAggregateAndProof), indexedAttestationSignatureSet, ]; // no need to write to SeenAttestationDatas if (!(await chain.bls.verifySignatureSets(signatureSets, { batchable: true, priority: prioritizeBls }))) { throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.INVALID_SIGNATURE }); } // 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.seenAggregators.isKnown(targetEpoch, aggregatorIndex)) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.AGGREGATOR_ALREADY_KNOWN, targetEpoch, aggregatorIndex, }); } // Same race-condition check as above for seen aggregators if (!skipValidationKnownAttesters && chain.seenAggregatedAttestations.isKnown(targetEpoch, committeeIndex, attDataRootHex, aggregationBits)) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.ATTESTERS_ALREADY_KNOWN, targetEpoch, aggregateRoot: attDataRootHex, }); } chain.seenAggregators.add(targetEpoch, aggregatorIndex); chain.seenAggregatedAttestations.add(targetEpoch, committeeIndex, attDataRootHex, { aggregationBits, trueBitCount: attestingIndices.length }, false); return { indexedAttestation, committeeValidatorIndices, attDataRootHex }; } //# sourceMappingURL=aggregateAndProof.js.map