@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
667 lines • 34.3 kB
JavaScript
import { ATTESTATION_SUBNET_COUNT, DOMAIN_BEACON_ATTESTER, ForkName, ForkSeq, SLOTS_PER_EPOCH, isForkPostElectra, } from "@lodestar/params";
import { EpochCacheError, EpochCacheErrorCode, computeEpochAtSlot, computeSigningRoot, computeStartSlotAtEpoch, createSingleSignatureSetFromComponents, } from "@lodestar/state-transition";
import { isElectraSingleAttestation, ssz, } from "@lodestar/types";
import { assert, toRootHex } from "@lodestar/utils";
import { MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC } from "../../constants/index.js";
import { sszDeserializeSingleAttestation } from "../../network/gossip/topic.js";
import { getShufflingDependentRoot } from "../../util/dependentRoot.js";
import { getAggregationBitsFromAttestationSerialized, getAttDataFromSignedAggregateAndProofElectra, getAttDataFromSignedAggregateAndProofPhase0, getAttesterIndexFromSingleAttestationSerialized, getCommitteeIndexFromSingleAttestationSerialized, getSignatureFromAttestationSerialized, getSignatureFromSingleAttestationSerialized, } from "../../util/sszBytes.js";
import { wrapError } from "../../util/wrapError.js";
import { AttestationError, AttestationErrorCode, GossipAction } from "../errors/index.js";
import { RegenCaller } from "../regen/index.js";
import { PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX, } from "../seenCache/seenAttestationData.js";
/**
* Verify gossip attestations of the same attestation data. The main advantage is we can batch verify bls signatures
* through verifySignatureSetsSameMessage bls api to improve performance.
* - If there are less than 2 signatures (minSameMessageSignatureSetsToBatch), verify each signature individually with batchable = true
* - do not prioritize bls signature set
*/
export async function validateGossipAttestationsSameAttData(fork, chain, attestationOrBytesArr,
// for unit test, consumers do not need to pass this
step0ValidationFn = validateAttestationNoSignatureCheck) {
if (attestationOrBytesArr.length === 0) {
return { results: [], batchableBls: false };
}
// step0: do all verifications except for signature verification
// this for await pattern below seems to be bad but it's not
// for seen AttestationData, it's the same to await Promise.all() pattern
// for unseen AttestationData, the 1st call will be cached and the rest will be fast
const step0ResultOrErrors = [];
for (const attestationOrBytes of attestationOrBytesArr) {
const { subnet } = attestationOrBytes;
const resultOrError = await wrapError(step0ValidationFn(fork, chain, attestationOrBytes, subnet));
step0ResultOrErrors.push(resultOrError);
}
// step1: verify signatures of all valid attestations
// map new index to index in resultOrErrors
const newIndexToOldIndex = new Map();
const signatureSets = [];
let newIndex = 0;
const step0Results = [];
for (const [i, resultOrError] of step0ResultOrErrors.entries()) {
if (resultOrError.err) {
continue;
}
step0Results.push(resultOrError.result);
newIndexToOldIndex.set(newIndex, i);
signatureSets.push(resultOrError.result.signatureSet);
newIndex++;
}
let signatureValids;
const batchableBls = signatureSets.length >= chain.opts.minSameMessageSignatureSetsToBatch;
if (batchableBls) {
// all signature sets should have same signing root since we filtered in network processor
signatureValids = await chain.bls.verifySignatureSetsSameMessage(signatureSets.map((set) => ({ publicKey: set.pubkey, signature: set.signature })), signatureSets[0].signingRoot);
}
else {
// 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, sigValid] 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 } = step0Results[i];
const targetEpoch = attestation.data.target.epoch;
if (sigValid) {
// 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)) {
step0ResultOrErrors[oldIndex] = {
err: new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN,
targetEpoch,
validatorIndex,
}),
};
}
// valid
chain.seenAttesters.add(targetEpoch, validatorIndex);
}
else {
step0ResultOrErrors[oldIndex] = {
err: new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.INVALID_SIGNATURE,
}),
};
}
}
return {
results: step0ResultOrErrors,
batchableBls,
};
}
/**
* Validate attestations from api
* - no need to deserialize attestation
* - no subnet
* - prioritize bls signature set
*/
export async function validateApiAttestation(fork, chain, attestationOrBytes) {
const prioritizeBls = true;
const subnet = null;
try {
const step0Result = await validateAttestationNoSignatureCheck(fork, chain, attestationOrBytes, subnet);
const { attestation, signatureSet, validatorIndex } = step0Result;
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 step0Result;
}
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.INVALID_SIGNATURE,
});
}
catch (err) {
if (err instanceof EpochCacheError && err.type.code === EpochCacheErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.BAD_TARGET_EPOCH,
});
}
throw err;
}
}
/**
* Only deserialize the single attestation if needed, use the cached AttestationData instead
* This is to avoid deserializing similar attestation multiple times which could help the gc
*/
async function validateAttestationNoSignatureCheck(fork, chain, attestationOrBytes,
/** Optional, to allow verifying attestations through API with unknown subnet */
subnet) {
// 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
// verify_early_checks
// Run the checks that happen before an indexed attestation is constructed.
let attestationOrCache;
let attDataKey = null;
if (attestationOrBytes.serializedData) {
// gossip
const attSlot = attestationOrBytes.attSlot;
attDataKey = getSeenAttDataKeyFromGossipAttestation(attestationOrBytes);
const committeeIndexForLookup = isForkPostElectra(fork)
? (getCommitteeIndexFromAttestationOrBytes(fork, attestationOrBytes) ?? 0)
: PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX;
const cachedAttData = attDataKey !== null ? chain.seenAttestationDatas.get(attSlot, committeeIndexForLookup, attDataKey) : null;
if (cachedAttData === null) {
const attestation = sszDeserializeSingleAttestation(fork, attestationOrBytes.serializedData);
// only deserialize on the first AttestationData that's not cached
attestationOrCache = { attestation, cache: null };
}
else {
attestationOrCache = { attestation: null, cache: cachedAttData, serializedData: attestationOrBytes.serializedData };
}
}
else {
// api
attDataKey = null;
attestationOrCache = { attestation: attestationOrBytes.attestation, cache: null };
}
const attData = attestationOrCache.attestation
? attestationOrCache.attestation.data
: attestationOrCache.cache.attestationData;
const attSlot = attData.slot;
const attEpoch = computeEpochAtSlot(attSlot);
const attTarget = attData.target;
const targetEpoch = attTarget.epoch;
let committeeIndex;
if (attestationOrCache.attestation) {
if (isElectraSingleAttestation(attestationOrCache.attestation)) {
// api or first time validation of a gossip attestation
committeeIndex = attestationOrCache.attestation.committeeIndex;
// [REJECT] attestation.data.index == 0
if (attData.index !== 0) {
throw new AttestationError(GossipAction.REJECT, { code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX });
}
}
else {
// phase0 attestation
committeeIndex = attData.index;
}
}
else {
// found a seen AttestationData
committeeIndex = attestationOrCache.cache.committeeIndex;
}
chain.metrics?.gossipAttestation.attestationSlotToClockSlot.observe({ caller: RegenCaller.validateGossipAttestation }, chain.clock.currentSlot - attSlot);
if (!attestationOrCache.cache) {
// [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] attestation.data.slot is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (within a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance)
// -- i.e. attestation.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= attestation.data.slot
// (a client MAY queue future attestations for processing at the appropriate slot).
// Post-deneb:
// [IGNORE] `attestation.data.slot` is equal to or earlier than the `current_slot` (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance)
// -- i.e. `attestation.data.slot <= current_slot`
// (a client MAY queue future attestation for processing at the appropriate slot).
// [IGNORE] the epoch of `attestation.data.slot` is either the current or previous epoch
// (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance)
// -- i.e. `compute_epoch_at_slot(attestation.data.slot) in (get_previous_epoch(state), get_current_epoch(state))`
verifyPropagationSlotRange(fork, chain, attestationOrCache.attestation.data.slot);
}
let aggregationBits = null;
let committeeValidatorIndex = null;
if (!isForkPostElectra(fork)) {
// [REJECT] The attestation is unaggregated -- that is, it has exactly one participating validator
// (len([bit for bit in attestation.aggregation_bits if bit]) == 1, i.e. exactly 1 bit is set).
// > TODO: Do this check **before** getting the target state but don't recompute zipIndexes
aggregationBits = attestationOrCache.attestation
? attestationOrCache.attestation.aggregationBits
: getAggregationBitsFromAttestationSerialized(attestationOrCache.serializedData);
if (aggregationBits === null) {
throw new AttestationError(GossipAction.REJECT, {
code: AttestationErrorCode.INVALID_SERIALIZED_BYTES,
});
}
const bitIndex = aggregationBits.getSingleTrueBit();
if (bitIndex === null) {
throw new AttestationError(GossipAction.REJECT, {
code: AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET,
});
}
committeeValidatorIndex = bitIndex;
}
let committeeValidatorIndices;
let getSigningRoot;
let expectedSubnet;
if (attestationOrCache.cache) {
committeeValidatorIndices = attestationOrCache.cache.committeeValidatorIndices;
const signingRoot = attestationOrCache.cache.signingRoot;
getSigningRoot = () => signingRoot;
expectedSubnet = attestationOrCache.cache.subnet;
}
else {
// Attestations must be for a known block. If the block is unknown, we simply drop the
// attestation and do not delay consideration for later.
//
// TODO (LH): Enforce a maximum skip distance for unaggregated attestations.
// [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).
const attHeadBlock = verifyHeadBlockAndTargetRoot(chain, attestationOrCache.attestation.data.beaconBlockRoot, attestationOrCache.attestation.data.target.root, attSlot, attEpoch, RegenCaller.validateGossipAttestation, chain.opts.maxSkipSlots);
// [REJECT] The block being voted for (attestation.data.beacon_block_root) passes validation.
// > Altready check in `verifyHeadBlockAndTargetRoot()`
// [IGNORE] The current finalized_checkpoint is an ancestor of the block defined by attestation.data.beacon_block_root
// -- i.e. get_ancestor(store, attestation.data.beacon_block_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == store.finalized_checkpoint.root
// > Altready check in `verifyHeadBlockAndTargetRoot()`
// [REJECT] The attestation's target block is an ancestor of the block named in the LMD vote
// --i.e. get_ancestor(store, attestation.data.beacon_block_root, compute_start_slot_at_epoch(attestation.data.target.epoch)) == attestation.data.target.root
// > Altready check in `verifyHeadBlockAndTargetRoot()`
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)
committeeValidatorIndices = getCommitteeIndices(shuffling, attSlot, committeeIndex);
getSigningRoot = () => getAttestationDataSigningRoot(chain.config, attData);
expectedSubnet = computeSubnetForSlot(shuffling, attSlot, committeeIndex);
}
let validatorIndex;
if (!isForkPostElectra(fork)) {
// The validity of aggregation bits are already checked above
assert.notNull(aggregationBits);
assert.notNull(committeeValidatorIndex);
validatorIndex = committeeValidatorIndices[committeeValidatorIndex];
// [REJECT] The number of aggregation bits matches the committee size
// -- i.e. len(attestation.aggregation_bits) == len(get_beacon_committee(state, data.slot, data.index)).
// > TODO: Is this necessary? Lighthouse does not do this check.
if (aggregationBits.bitLen !== committeeValidatorIndices.length) {
throw new AttestationError(GossipAction.REJECT, {
code: AttestationErrorCode.WRONG_NUMBER_OF_AGGREGATION_BITS,
});
}
}
else {
if (attestationOrCache.attestation) {
validatorIndex = attestationOrCache.attestation.attesterIndex;
}
else {
const attesterIndex = getAttesterIndexFromSingleAttestationSerialized(attestationOrCache.serializedData);
if (attesterIndex === null) {
throw new AttestationError(GossipAction.REJECT, {
code: AttestationErrorCode.INVALID_SERIALIZED_BYTES,
});
}
validatorIndex = attesterIndex;
}
// [REJECT] The attester is a member of the committee -- i.e.
// `attestation.attester_index in get_beacon_committee(state, attestation.data.slot, index)`.
// Position of the validator in its committee
committeeValidatorIndex = committeeValidatorIndices.indexOf(validatorIndex);
if (committeeValidatorIndex === -1) {
throw new AttestationError(GossipAction.REJECT, {
code: AttestationErrorCode.ATTESTER_NOT_IN_COMMITTEE,
});
}
}
// LH > verify_middle_checks
// Run the checks that apply to the indexed attestation before the signature is checked.
// Check correct subnet
// The attestation is the first valid attestation received for the participating validator for the slot, attestation.data.slot.
// [REJECT] The attestation is for the correct subnet
// -- i.e. compute_subnet_for_attestation(committees_per_slot, attestation.data.slot, attestation.data.index) == subnet_id,
// where committees_per_slot = get_committee_count_per_slot(state, attestation.data.target.epoch),
// which may be pre-computed along with the committee information for the signature check.
if (subnet !== null && subnet !== expectedSubnet) {
throw new AttestationError(GossipAction.REJECT, {
code: AttestationErrorCode.INVALID_SUBNET_ID,
received: subnet,
expected: expectedSubnet,
});
}
// [IGNORE] There has been no other valid attestation seen on an attestation subnet that has an
// identical attestation.data.target.epoch and participating validator index.
if (chain.seenAttesters.isKnown(targetEpoch, validatorIndex)) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN,
targetEpoch,
validatorIndex,
});
}
// [REJECT] The signature of attestation is valid.
const attestingIndices = [validatorIndex];
let signatureSet;
let attDataRootHex;
const signature = attestationOrCache.attestation
? attestationOrCache.attestation.signature
: !isForkPostElectra(fork)
? getSignatureFromAttestationSerialized(attestationOrCache.serializedData)
: getSignatureFromSingleAttestationSerialized(attestationOrCache.serializedData);
if (signature === null) {
throw new AttestationError(GossipAction.REJECT, {
code: AttestationErrorCode.INVALID_SERIALIZED_BYTES,
});
}
if (attestationOrCache.cache) {
// there could be up to 6% of cpu time to compute signing root if we don't clone the signature set
signatureSet = createSingleSignatureSetFromComponents(chain.index2pubkey[validatorIndex], attestationOrCache.cache.signingRoot, signature);
attDataRootHex = attestationOrCache.cache.attDataRootHex;
}
else {
signatureSet = createSingleSignatureSetFromComponents(chain.index2pubkey[validatorIndex], getSigningRoot(), signature);
// add cached attestation data before verifying signature
attDataRootHex = toRootHex(ssz.phase0.AttestationData.hashTreeRoot(attData));
if (attDataKey) {
// for pre-electra, committee index key is 0. See SeenAttestationDatas.add() documentation
const committeeIndexKey = isForkPostElectra(fork)
? committeeIndex
: PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX;
chain.seenAttestationDatas.add(attSlot, committeeIndexKey, attDataKey, {
committeeValidatorIndices,
committeeIndex,
signingRoot: signatureSet.signingRoot,
subnet: expectedSubnet,
// precompute this to be used in forkchoice
// root of AttestationData was already cached during getIndexedAttestationSignatureSet
attDataRootHex,
attestationData: attData,
});
}
}
// no signature check, leave that for step1
const indexedAttestation = {
attestingIndices,
data: attData,
signature,
};
const attestation = attestationOrCache.attestation
? attestationOrCache.attestation
: !isForkPostElectra(fork)
? {
// Aggregation bits are already asserted above to not be null
aggregationBits: aggregationBits,
data: attData,
signature,
}
: {
committeeIndex,
attesterIndex: validatorIndex,
data: attData,
signature,
};
return {
attestation,
indexedAttestation,
subnet: expectedSubnet,
attDataRootHex,
signatureSet,
validatorIndex,
committeeIndex,
committeeValidatorIndex,
committeeSize: committeeValidatorIndices.length,
};
}
/**
* Verify that the `attestation` is within the acceptable gossip propagation range, with reference
* to the current slot of the `chain`.
*
* Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
* Note: We do not queue future attestations for later processing
*/
export function verifyPropagationSlotRange(fork, chain, attestationSlot) {
// slot with future tolerance of MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC
const latestPermissibleSlot = chain.clock.slotWithFutureTolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC);
if (attestationSlot > latestPermissibleSlot) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.FUTURE_SLOT,
latestPermissibleSlot,
attestationSlot,
});
}
// Post deneb the attestations are valid for current as well as previous epoch
// while pre deneb they are valid for ATTESTATION_PROPAGATION_SLOT_RANGE
//
// see: https://github.com/ethereum/consensus-specs/pull/3360
if (ForkSeq[fork] < ForkSeq.deneb) {
const earliestPermissibleSlot = Math.max(
// slot with past tolerance of MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC
// ATTESTATION_PROPAGATION_SLOT_RANGE = SLOTS_PER_EPOCH
chain.clock.slotWithPastTolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC) - SLOTS_PER_EPOCH, 0);
if (attestationSlot < earliestPermissibleSlot) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.PAST_SLOT,
earliestPermissibleSlot,
attestationSlot,
});
}
}
else {
const attestationEpoch = computeEpochAtSlot(attestationSlot);
// upper bound for current epoch is same as epoch of latestPermissibleSlot
const latestPermissibleCurrentEpoch = computeEpochAtSlot(latestPermissibleSlot);
if (attestationEpoch > latestPermissibleCurrentEpoch) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.FUTURE_EPOCH,
currentEpoch: latestPermissibleCurrentEpoch,
attestationEpoch,
});
}
// lower bound for previous epoch is same as epoch of earliestPermissibleSlot
const currentEpochWithPastTolerance = computeEpochAtSlot(chain.clock.slotWithPastTolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC));
const earliestPermissiblePreviousEpoch = Math.max(currentEpochWithPastTolerance - 1, 0);
if (attestationEpoch < earliestPermissiblePreviousEpoch) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.PAST_EPOCH,
previousEpoch: earliestPermissiblePreviousEpoch,
attestationEpoch,
});
}
}
}
/**
* Verify:
* 1. head block is known
* 2. attestation's target block is an ancestor of the block named in the LMD vote
*/
export function verifyHeadBlockAndTargetRoot(chain, beaconBlockRoot, targetRoot, attestationSlot, attestationEpoch, caller, maxSkipSlots) {
const headBlock = verifyHeadBlockIsKnown(chain, beaconBlockRoot);
// Lighthouse rejects the attestation, however Lodestar only ignores considering it's not against the spec
// it's more about a DOS protection to us
// With verifyPropagationSlotRange() and maxSkipSlots = 32, it's unlikely we have to regenerate states in queue
// to validate beacon_attestation and aggregate_and_proof
const slotDistance = attestationSlot - headBlock.slot;
chain.metrics?.gossipAttestation.headSlotToAttestationSlot.observe({ caller }, slotDistance);
if (maxSkipSlots !== undefined && slotDistance > maxSkipSlots) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.TOO_MANY_SKIPPED_SLOTS,
attestationSlot,
headBlockSlot: headBlock.slot,
});
}
verifyAttestationTargetRoot(headBlock, targetRoot, attestationEpoch);
return headBlock;
}
/**
* Get a shuffling for attestation verification from the ShufflingCache.
* - if blockEpoch is attEpoch, use current shuffling of head state
* - if blockEpoch is attEpoch - 1, use next shuffling of head state
* - if blockEpoch is less than attEpoch - 1, dial head state to attEpoch - 1, and add to ShufflingCache
*
* This implementation does not require to dial head state to attSlot at fork boundary because we always get domain of attSlot
* in consumer context.
*
* This is similar to the old getStateForAttestationVerification
* see https://github.com/ChainSafe/lodestar/blob/v1.11.3/packages/beacon-node/src/chain/validation/attestation.ts#L566
*/
export async function getShufflingForAttestationVerification(chain, attEpoch, attHeadBlock, regenCaller) {
const blockEpoch = computeEpochAtSlot(attHeadBlock.slot);
const shufflingDependentRoot = getShufflingDependentRoot(chain.forkChoice, attEpoch, blockEpoch, attHeadBlock);
const shuffling = await chain.shufflingCache.get(attEpoch, shufflingDependentRoot);
if (shuffling) {
// most of the time, we should get the shuffling from cache
chain.metrics?.gossipAttestation.shufflingCacheHit.inc({ caller: regenCaller });
return shuffling;
}
chain.metrics?.gossipAttestation.shufflingCacheMiss.inc({ caller: regenCaller });
try {
// for the 1st time of the same epoch and dependent root, it awaits for the regen state
// from the 2nd time, it should use the same cached promise and it should reach the above code
chain.metrics?.gossipAttestation.shufflingCacheRegenHit.inc({ caller: regenCaller });
return await chain.regenStateForAttestationVerification(attEpoch, shufflingDependentRoot, attHeadBlock, regenCaller);
}
catch (e) {
chain.metrics?.gossipAttestation.shufflingCacheRegenMiss.inc({ caller: regenCaller });
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.MISSING_STATE_TO_VERIFY_ATTESTATION,
error: e,
});
}
}
/**
* Different version of getAttestationDataSigningRoot in state-transition which doesn't require a state.
*/
export function getAttestationDataSigningRoot(config, data) {
const slot = computeStartSlotAtEpoch(data.target.epoch);
// previously, we call `domain = config.getDomain(state.slot, DOMAIN_BEACON_ATTESTER, slot)`
// at fork boundary, it's required to dial to target epoch https://github.com/ChainSafe/lodestar/blob/v1.11.3/packages/beacon-node/src/chain/validation/attestation.ts#L573
// instead of that, just use the fork at slot in the attestation data
const fork = config.getForkName(slot);
const domain = config.getDomainAtFork(fork, DOMAIN_BEACON_ATTESTER);
return computeSigningRoot(ssz.phase0.AttestationData, data, domain);
}
/**
* Checks if the `attestation.data.beaconBlockRoot` is known to this chain.
*
* The block root may not be known for two reasons:
*
* 1. The block has never been verified by our application.
* 2. The block is prior to the latest finalized block.
*
* Case (1) is the exact thing we're trying to detect. However case (2) is a little different, but
* it's still fine to ignore here because there's no need for us to handle attestations that are
* already finalized.
*/
function verifyHeadBlockIsKnown(chain, beaconBlockRoot) {
// TODO (LH): Enforce a maximum skip distance for unaggregated attestations.
const headBlock = chain.forkChoice.getBlock(beaconBlockRoot);
if (headBlock === null) {
throw new AttestationError(GossipAction.IGNORE, {
code: AttestationErrorCode.UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT,
root: toRootHex(beaconBlockRoot),
});
}
return headBlock;
}
/**
* Verifies that the `attestation.data.target.root` is indeed the target root of the block at
* `attestation.data.beacon_block_root`.
*/
function verifyAttestationTargetRoot(headBlock, targetRoot, attestationEpoch) {
// Check the attestation target root.
const headBlockEpoch = computeEpochAtSlot(headBlock.slot);
if (headBlockEpoch > attestationEpoch) {
// The epoch references an invalid head block from a future epoch.
//
// This check is not in the specification, however we guard against it since it opens us up
// to weird edge cases during verification.
//
// Whilst this attestation *technically* could be used to add value to a block, it is
// invalid in the spirit of the protocol. Here we choose safety over profit.
//
// Reference:
// https://github.com/ethereum/consensus-specs/pull/2001#issuecomment-699246659
throw new AttestationError(GossipAction.REJECT, {
code: AttestationErrorCode.INVALID_TARGET_ROOT,
targetRoot: toRootHex(targetRoot),
expected: null,
});
}
const expectedTargetRoot = headBlockEpoch === attestationEpoch
? // If the block is in the same epoch as the attestation, then use the target root
// from the block.
headBlock.targetRoot
: // If the head block is from a previous epoch then skip slots will cause the head block
// root to become the target block root.
//
// We know the head block is from a previous epoch due to a previous check.
headBlock.blockRoot;
// TODO: Do a fast comparision to convert and compare byte by byte
if (expectedTargetRoot !== toRootHex(targetRoot)) {
// Reject any attestation with an invalid target root.
throw new AttestationError(GossipAction.REJECT, {
code: AttestationErrorCode.INVALID_TARGET_ROOT,
targetRoot: toRootHex(targetRoot),
expected: expectedTargetRoot,
});
}
}
/**
* Get a list of indices of validators in the given committee
* attestationIndex - Index of the committee in shuffling.committees
*/
export function getCommitteeIndices(shuffling, attestationSlot, attestationIndex) {
const { committees } = shuffling;
const slotCommittees = committees[attestationSlot % SLOTS_PER_EPOCH];
if (attestationIndex >= slotCommittees.length) {
throw new AttestationError(GossipAction.REJECT, {
code: AttestationErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE,
index: attestationIndex,
});
}
return slotCommittees[attestationIndex];
}
/**
* Compute the correct subnet for a slot/committee index
*/
export function computeSubnetForSlot(shuffling, slot, committeeIndex) {
const slotsSinceEpochStart = slot % SLOTS_PER_EPOCH;
const committeesSinceEpochStart = shuffling.committeesPerSlot * slotsSinceEpochStart;
return (committeesSinceEpochStart + committeeIndex) % ATTESTATION_SUBNET_COUNT;
}
/**
* Return fork-dependent seen attestation key
* - for pre-electra, it's the AttestationData base64 from Attestation
* - for electra and later, it's the AttestationData base64 from SingleAttestation
* - consumers need to also pass slot + committeeIndex to get the correct SeenAttestationData
*/
export function getSeenAttDataKeyFromGossipAttestation(attestation) {
// SeenAttDataKey is the same as gossip index
return attestation.attDataBase64;
}
/**
* Extract attestation data key from SignedAggregateAndProof Uint8Array to use cached data from SeenAttestationDatas
* - for both electra + pre-electra, it's the AttestationData base64
* - consumers need to also pass slot + committeeIndex to get the correct SeenAttestationData
*/
export function getSeenAttDataKeyFromSignedAggregateAndProof(fork, aggregateAndProof) {
return isForkPostElectra(fork)
? getAttDataFromSignedAggregateAndProofElectra(aggregateAndProof)
: getAttDataFromSignedAggregateAndProofPhase0(aggregateAndProof);
}
export function getCommitteeIndexFromAttestationOrBytes(fork, attestationOrBytes) {
const isGossipAttestation = attestationOrBytes.serializedData !== null;
if (isForkPostElectra(fork)) {
if (isGossipAttestation) {
return getCommitteeIndexFromSingleAttestationSerialized(ForkName.electra, attestationOrBytes.serializedData);
}
return attestationOrBytes.attestation.committeeIndex;
}
if (isGossipAttestation) {
return getCommitteeIndexFromSingleAttestationSerialized(ForkName.phase0, attestationOrBytes.serializedData);
}
return attestationOrBytes.attestation.data.index;
}
/**
* Convert pre-electra single attestation (`phase0.Attestation`) to post-electra `SingleAttestation`
*/
export function toElectraSingleAttestation(attestation, attesterIndex) {
return {
committeeIndex: attestation.data.index,
attesterIndex,
data: attestation.data,
signature: attestation.signature,
};
}
//# sourceMappingURL=attestation.js.map