@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
124 lines (105 loc) • 6.06 kB
text/typescript
import {SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar/params";
import {IBeaconStateView, isStatePostAltair, isSyncCommitteeAggregator} from "@lodestar/state-transition";
import {ValidatorIndex, altair} from "@lodestar/types";
import {GossipAction, SyncCommitteeError, SyncCommitteeErrorCode} from "../errors/index.js";
import {IBeaconChain} from "../interface.js";
import {
getContributionAndProofSignatureSet,
getSyncCommitteeContributionSignatureSet,
getSyncCommitteeSelectionProofSignatureSet,
} from "./signatureSets/index.js";
import {validateGossipSyncCommitteeExceptSig} from "./syncCommittee.js";
/**
* Spec v1.1.0-beta.2
*/
export async function validateSyncCommitteeGossipContributionAndProof(
chain: IBeaconChain,
signedContributionAndProof: altair.SignedContributionAndProof,
skipValidationKnownParticipants = false
): Promise<{syncCommitteeParticipantIndices: ValidatorIndex[]}> {
const contributionAndProof = signedContributionAndProof.message;
const {contribution, aggregatorIndex} = contributionAndProof;
const {subcommitteeIndex, slot} = contribution;
const headState = chain.getHeadState();
validateGossipSyncCommitteeExceptSig(chain, headState, subcommitteeIndex, {
slot,
validatorIndex: contributionAndProof.aggregatorIndex,
});
// [IGNORE] The contribution's slot is for the current slot, i.e. contribution.slot == current_slot.
// > Checked in validateGossipSyncCommitteeExceptSig()
// [REJECT] The aggregator's validator index is in the declared subcommittee of the current sync committee
// -- i.e. state.validators[contribution_and_proof.aggregator_index].pubkey in
// get_sync_subcommittee_pubkeys(state, contribution.subcommittee_index).
// > Checked in validateGossipSyncCommitteeExceptSig()
// _[IGNORE]_ A valid sync committee contribution with equal `slot`, `beacon_block_root` and `subcommittee_index` whose
// `aggregation_bits` is non-strict superset has _not_ already been seen.
if (!skipValidationKnownParticipants && chain.seenContributionAndProof.participantsKnown(contribution)) {
throw new SyncCommitteeError(GossipAction.IGNORE, {
code: SyncCommitteeErrorCode.SYNC_COMMITTEE_PARTICIPANTS_ALREADY_KNOWN,
});
}
// [IGNORE] The sync committee contribution is the first valid contribution received for the aggregator with index
// contribution_and_proof.aggregator_index for the slot contribution.slot and subcommittee index contribution.subcommittee_index.
if (chain.seenContributionAndProof.isAggregatorKnown(slot, subcommitteeIndex, aggregatorIndex)) {
throw new SyncCommitteeError(GossipAction.IGNORE, {
code: SyncCommitteeErrorCode.SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN,
});
}
// [REJECT] The contribution has participants -- that is, any(contribution.aggregation_bits)
const syncCommitteeParticipantIndices = getContributionIndices(headState, contribution);
if (syncCommitteeParticipantIndices.length === 0) {
throw new SyncCommitteeError(GossipAction.REJECT, {
code: SyncCommitteeErrorCode.NO_PARTICIPANT,
});
}
// [REJECT] contribution_and_proof.selection_proof selects the validator as an aggregator for the slot --
// i.e. is_sync_committee_aggregator(contribution_and_proof.selection_proof) returns True.
if (!isSyncCommitteeAggregator(contributionAndProof.selectionProof)) {
throw new SyncCommitteeError(GossipAction.REJECT, {
code: SyncCommitteeErrorCode.INVALID_AGGREGATOR,
aggregatorIndex: contributionAndProof.aggregatorIndex,
});
}
// [REJECT] The aggregator's validator index is in the declared subcommittee of the current sync committee --
// i.e. state.validators[contribution_and_proof.aggregator_index].pubkey in get_sync_subcommittee_pubkeys(state, contribution.subcommittee_index).
// > Checked in validateGossipSyncCommitteeExceptSig()
const signatureSets = [
// [REJECT] The contribution_and_proof.selection_proof is a valid signature of the SyncAggregatorSelectionData
// derived from the contribution by the validator with index contribution_and_proof.aggregator_index.
getSyncCommitteeSelectionProofSignatureSet(chain.config, headState, contributionAndProof),
// [REJECT] The aggregator signature, signed_contribution_and_proof.signature, is valid.
getContributionAndProofSignatureSet(chain.config, headState, signedContributionAndProof),
// [REJECT] The aggregate signature is valid for the message beacon_block_root and aggregate pubkey derived from
// the participation info in aggregation_bits for the subcommittee specified by the contribution.subcommittee_index.
getSyncCommitteeContributionSignatureSet(chain.config, headState, contribution, syncCommitteeParticipantIndices),
];
if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true}))) {
throw new SyncCommitteeError(GossipAction.REJECT, {
code: SyncCommitteeErrorCode.INVALID_SIGNATURE,
});
}
// no need to add to seenSyncCommittteeContributionCache here, gossip handler will do that
chain.seenContributionAndProof.add(contributionAndProof, syncCommitteeParticipantIndices.length);
return {syncCommitteeParticipantIndices};
}
/**
* Retrieve pubkeys in contribution aggregate using epochCtx:
* - currSyncCommitteeIndexes cache
* - pubkeyCache
*/
function getContributionIndices(
state: IBeaconStateView,
contribution: altair.SyncCommitteeContribution
): ValidatorIndex[] {
if (!isStatePostAltair(state)) {
throw new Error("Expected Altair state for sync committee contribution");
}
const startIndex = contribution.subcommitteeIndex * SYNC_COMMITTEE_SUBNET_SIZE;
const syncCommittee = state.getIndexedSyncCommittee(contribution.slot);
// The bits in contribution.aggregationBits select validatorIndexes in the subcommittee starting at startIndex
const subcommitteeValidatorIndices = syncCommittee.validatorIndices.slice(
startIndex,
startIndex + SYNC_COMMITTEE_SUBNET_SIZE
);
return contribution.aggregationBits.intersectValues(subcommitteeValidatorIndices);
}