Skip to content

Commit

Permalink
feat: beacon node process electra attestations EIP-7549 (#6738)
Browse files Browse the repository at this point in the history
* Process attestations in block

* Fix check-types

* Address comments
  • Loading branch information
ensi321 authored and g11tech committed Jul 30, 2024
1 parent 5fe03b4 commit e27fe8c
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 35 deletions.
4 changes: 3 additions & 1 deletion packages/beacon-node/src/chain/blocks/importBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export async function importBlock(
const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch;
const blockDelaySec = (fullyVerifiedBlock.seenTimestampSec - postState.genesisTime) % this.config.SECONDS_PER_SLOT;
const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000);
const fork = this.config.getForkSeq(blockSlot);

// this is just a type assertion since blockinput with dataPromise type will not end up here
if (blockInput.type === BlockInputType.dataPromise) {
Expand Down Expand Up @@ -148,7 +149,8 @@ export async function importBlock(

for (const attestation of attestations) {
try {
const indexedAttestation = postState.epochCtx.getIndexedAttestation(attestation);
// TODO Electra: figure out how to reuse the attesting indices computed from state transition
const indexedAttestation = postState.epochCtx.getIndexedAttestation(fork, attestation);
const {target, beaconBlockRoot} = attestation.data;

const attDataRoot = toHexString(ssz.phase0.AttestationData.hashTreeRoot(indexedAttestation.data));
Expand Down
5 changes: 4 additions & 1 deletion packages/beacon-node/test/spec/presets/fork_choice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@ const forkChoiceTest =
if (!attestation) throw Error(`No attestation ${step.attestation}`);
const headState = chain.getHeadState();
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(attestation.data));
chain.forkChoice.onAttestation(headState.epochCtx.getIndexedAttestation(attestation), attDataRootHex);
chain.forkChoice.onAttestation(
headState.epochCtx.getIndexedAttestation(ForkSeq[fork], attestation),
attDataRootHex
);
}

// attester slashing step
Expand Down
57 changes: 43 additions & 14 deletions packages/state-transition/src/block/processAttestationPhase0.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {toHexString} from "@chainsafe/ssz";
import {Slot, phase0, ssz} from "@lodestar/types";
import {Slot, allForks, electra, phase0, ssz} from "@lodestar/types";

import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH, ForkSeq} from "@lodestar/params";
import {assert} from "@lodestar/utils";
import {computeEpochAtSlot} from "../util/index.js";
import {CachedBeaconStatePhase0, CachedBeaconStateAllForks} from "../types.js";
import {isValidIndexedAttestation} from "./index.js";
Expand Down Expand Up @@ -51,27 +52,22 @@ export function processAttestationPhase0(
state.previousEpochAttestations.push(pendingAttestation);
}

if (!isValidIndexedAttestation(state, epochCtx.getIndexedAttestation(attestation), verifySignature)) {
if (!isValidIndexedAttestation(state, epochCtx.getIndexedAttestation(ForkSeq.phase0, attestation), verifySignature)) {
throw new Error("Attestation is not valid");
}
}

export function validateAttestation(
fork: ForkSeq,
state: CachedBeaconStateAllForks,
attestation: phase0.Attestation
attestation: allForks.Attestation
): void {
const {epochCtx} = state;
const slot = state.slot;
const data = attestation.data;
const computedEpoch = computeEpochAtSlot(data.slot);
const committeeCount = epochCtx.getCommitteeCountPerSlot(computedEpoch);
if (!(data.index < committeeCount)) {
throw new Error(
"Attestation committee index not within current committee count: " +
`committeeIndex=${data.index} committeeCount=${committeeCount}`
);
}

if (!(data.target.epoch === epochCtx.previousShuffling.epoch || data.target.epoch === epochCtx.epoch)) {
throw new Error(
"Attestation target epoch not in previous or current epoch: " +
Expand All @@ -93,12 +89,45 @@ export function validateAttestation(
);
}

const committee = epochCtx.getBeaconCommittee(data.slot, data.index);
if (attestation.aggregationBits.bitLen !== committee.length) {
throw new Error(
"Attestation aggregation bits length does not match committee length: " +
`aggregationBitsLength=${attestation.aggregationBits.bitLen} committeeLength=${committee.length}`
if (fork >= ForkSeq.electra) {
assert.equal(data.index, 0, `AttestationData.index must be zero: index=${data.index}`);
const attestationElectra = attestation as electra.Attestation;
const committeeBitsLength = attestationElectra.committeeBits.bitLen;

if (committeeBitsLength > committeeCount) {
throw new Error(
`Attestation committee bits length are longer than number of committees: committeeBitsLength=${committeeBitsLength} numCommittees=${committeeCount}`
);
}

// TODO Electra: this should be obsolete soon when the spec switches to committeeIndices
const committeeIndices = attestationElectra.committeeBits.getTrueBitIndexes();

// Get total number of attestation participant of every committee specified
const participantCount = committeeIndices
.map((committeeIndex) => epochCtx.getBeaconCommittee(data.slot, committeeIndex).length)
.reduce((acc, committeeSize) => acc + committeeSize, 0);

assert.equal(
attestationElectra.aggregationBits.bitLen,
participantCount,
`Attestation aggregation bits length does not match total number of committee participant aggregationBitsLength=${attestation.aggregationBits.bitLen} participantCount=${participantCount}`
);
} else {
if (!(data.index < committeeCount)) {
throw new Error(
"Attestation committee index not within current committee count: " +
`committeeIndex=${data.index} committeeCount=${committeeCount}`
);
}

const committee = epochCtx.getBeaconCommittee(data.slot, data.index);
if (attestation.aggregationBits.bitLen !== committee.length) {
throw new Error(
"Attestation aggregation bits length does not match committee length: " +
`aggregationBitsLength=${attestation.aggregationBits.bitLen} committeeLength=${committee.length}`
);
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/state-transition/src/block/processAttestations.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {phase0} from "@lodestar/types";
import {allForks} from "@lodestar/types";
import {ForkSeq} from "@lodestar/params";
import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStatePhase0} from "../types.js";
import {processAttestationPhase0} from "./processAttestationPhase0.js";
Expand All @@ -10,7 +10,7 @@ import {processAttestationsAltair} from "./processAttestationsAltair.js";
export function processAttestations(
fork: ForkSeq,
state: CachedBeaconStateAllForks,
attestations: phase0.Attestation[],
attestations: allForks.Attestation[],
verifySignatures = true
): void {
if (fork === ForkSeq.phase0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {byteArrayEquals} from "@chainsafe/ssz";
import {Epoch, phase0} from "@lodestar/types";
import {Epoch, allForks, phase0} from "@lodestar/types";
import {intSqrt} from "@lodestar/utils";

import {
Expand Down Expand Up @@ -32,7 +32,7 @@ const SLOTS_PER_EPOCH_SQRT = intSqrt(SLOTS_PER_EPOCH);
export function processAttestationsAltair(
fork: ForkSeq,
state: CachedBeaconStateAltair,
attestations: phase0.Attestation[],
attestations: allForks.Attestation[],
verifySignature = true
): void {
const {epochCtx} = state;
Expand All @@ -49,8 +49,7 @@ export function processAttestationsAltair(
validateAttestation(fork, state, attestation);

// Retrieve the validator indices from the attestation participation bitfield
const committeeIndices = epochCtx.getBeaconCommittee(data.slot, data.index);
const attestingIndices = attestation.aggregationBits.intersectValues(committeeIndices);
const attestingIndices = epochCtx.getAttestingIndices(fork, attestation);

// this check is done last because its the most expensive (if signature verification is toggled on)
// TODO: Why should we verify an indexed attestation that we just created? If it's just for the signature
Expand Down
90 changes: 78 additions & 12 deletions packages/state-transition/src/cache/epochCache.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import {PublicKey} from "@chainsafe/blst";
import * as immutable from "immutable";
import {fromHexString} from "@chainsafe/ssz";
import {BLSSignature, CommitteeIndex, Epoch, Slot, ValidatorIndex, phase0, SyncPeriod} from "@lodestar/types";
import {
BLSSignature,
CommitteeIndex,
Epoch,
Slot,
ValidatorIndex,
phase0,
SyncPeriod,
allForks,
electra,
} from "@lodestar/types";
import {createBeaconConfig, BeaconConfig, ChainConfig} from "@lodestar/config";
import {
ATTESTATION_SUBNET_COUNT,
Expand Down Expand Up @@ -644,15 +654,47 @@ export class EpochCache {
* Return the beacon committee at slot for index.
*/
getBeaconCommittee(slot: Slot, index: CommitteeIndex): Uint32Array {
return this.getBeaconCommittees(slot, [index]);
}

/**
* Return a single Uint32Array representing concatted committees of indices
*/
getBeaconCommittees(slot: Slot, indices: CommitteeIndex[]): Uint32Array {
if (indices.length === 0) {
throw new Error("Attempt to get committees without providing CommitteeIndex");
}

const slotCommittees = this.getShufflingAtSlot(slot).committees[slot % SLOTS_PER_EPOCH];
if (index >= slotCommittees.length) {
throw new EpochCacheError({
code: EpochCacheErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE,
index,
maxIndex: slotCommittees.length,
});
const committees = [];

for (const index of indices) {
if (index >= slotCommittees.length) {
throw new EpochCacheError({
code: EpochCacheErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE,
index,
maxIndex: slotCommittees.length,
});
}
committees.push(slotCommittees[index]);
}

// Early return if only one index
if (committees.length === 1) {
return committees[0];
}

// Create a new Uint32Array to flatten `committees`
const totalLength = committees.reduce((acc, curr) => acc + curr.length, 0);
const result = new Uint32Array(totalLength);

let offset = 0;
for (const committee of committees) {
result.set(committee, offset);
offset += committee.length;
}
return slotCommittees[index];

return result;
}

getCommitteeCountPerSlot(epoch: Epoch): number {
Expand Down Expand Up @@ -738,10 +780,9 @@ export class EpochCache {
/**
* Return the indexed attestation corresponding to ``attestation``.
*/
getIndexedAttestation(attestation: phase0.Attestation): phase0.IndexedAttestation {
const {aggregationBits, data} = attestation;
const committeeIndices = this.getBeaconCommittee(data.slot, data.index);
const attestingIndices = aggregationBits.intersectValues(committeeIndices);
getIndexedAttestation(fork: ForkSeq, attestation: allForks.Attestation): allForks.IndexedAttestation {
const {data} = attestation;
const attestingIndices = this.getAttestingIndices(fork, attestation);

// sort in-place
attestingIndices.sort((a, b) => a - b);
Expand All @@ -752,6 +793,31 @@ export class EpochCache {
};
}

/**
* Return indices of validators who attestested in `attestation`
*/
getAttestingIndices(fork: ForkSeq, attestation: allForks.Attestation): number[] {
if (fork < ForkSeq.electra) {
const {aggregationBits, data} = attestation;
const validatorIndices = this.getBeaconCommittee(data.slot, data.index);

return aggregationBits.intersectValues(validatorIndices);
} else {
const {aggregationBits, committeeBits, data} = attestation as electra.Attestation;

// There is a naming conflict on the term `committeeIndices`
// In Lodestar it usually means a list of validator indices of participants in a committee
// In the spec it means a list of committee indices according to committeeBits
// This `committeeIndices` refers to the latter
// TODO Electra: resolve the naming conflicts
const committeeIndices = committeeBits.getTrueBitIndexes();

const validatorIndices = this.getBeaconCommittees(data.slot, committeeIndices);

return aggregationBits.intersectValues(validatorIndices);
}
}

getCommitteeAssignments(
epoch: Epoch,
requestedValidatorIndices: ValidatorIndex[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ export function getAttestationsSignatureSets(
state: CachedBeaconStateAllForks,
signedBlock: SignedBeaconBlock
): ISignatureSet[] {
// TODO: figure how to get attesting indices of an attestation once per block processing
return signedBlock.message.body.attestations.map((attestation) =>
getIndexedAttestationSignatureSet(state, state.epochCtx.getIndexedAttestation(attestation))
getIndexedAttestationSignatureSet(
state,
state.epochCtx.getIndexedAttestation(state.epochCtx.config.getForkSeq(signedBlock.message.slot), attestation)
)
);
}

0 comments on commit e27fe8c

Please sign in to comment.