Skip to content

Commit 86b15bc

Browse files
ensi321g11tech
authored andcommitted
feat: beacon node process electra attestations EIP-7549 (ChainSafe#6738)
* Process attestations in block * Fix check-types * Address comments
1 parent 6ae385a commit 86b15bc

File tree

7 files changed

+138
-35
lines changed

7 files changed

+138
-35
lines changed

packages/beacon-node/src/chain/blocks/importBlock.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export async function importBlock(
6969
const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch;
7070
const blockDelaySec = (fullyVerifiedBlock.seenTimestampSec - postState.genesisTime) % this.config.SECONDS_PER_SLOT;
7171
const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000);
72+
const fork = this.config.getForkSeq(blockSlot);
7273

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

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

154156
const attDataRoot = toHexString(ssz.phase0.AttestationData.hashTreeRoot(indexedAttestation.data));

packages/beacon-node/test/spec/presets/fork_choice.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,10 @@ const forkChoiceTest =
137137
if (!attestation) throw Error(`No attestation ${step.attestation}`);
138138
const headState = chain.getHeadState();
139139
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(attestation.data));
140-
chain.forkChoice.onAttestation(headState.epochCtx.getIndexedAttestation(attestation), attDataRootHex);
140+
chain.forkChoice.onAttestation(
141+
headState.epochCtx.getIndexedAttestation(ForkSeq[fork], attestation),
142+
attDataRootHex
143+
);
141144
}
142145

143146
// attester slashing step

packages/state-transition/src/block/processAttestationPhase0.ts

+43-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {toHexString} from "@chainsafe/ssz";
2-
import {Slot, phase0, ssz} from "@lodestar/types";
2+
import {Slot, allForks, electra, phase0, ssz} from "@lodestar/types";
33

44
import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH, ForkSeq} from "@lodestar/params";
5+
import {assert} from "@lodestar/utils";
56
import {computeEpochAtSlot} from "../util/index.js";
67
import {CachedBeaconStatePhase0, CachedBeaconStateAllForks} from "../types.js";
78
import {isValidIndexedAttestation} from "./index.js";
@@ -51,27 +52,22 @@ export function processAttestationPhase0(
5152
state.previousEpochAttestations.push(pendingAttestation);
5253
}
5354

54-
if (!isValidIndexedAttestation(state, epochCtx.getIndexedAttestation(attestation), verifySignature)) {
55+
if (!isValidIndexedAttestation(state, epochCtx.getIndexedAttestation(ForkSeq.phase0, attestation), verifySignature)) {
5556
throw new Error("Attestation is not valid");
5657
}
5758
}
5859

5960
export function validateAttestation(
6061
fork: ForkSeq,
6162
state: CachedBeaconStateAllForks,
62-
attestation: phase0.Attestation
63+
attestation: allForks.Attestation
6364
): void {
6465
const {epochCtx} = state;
6566
const slot = state.slot;
6667
const data = attestation.data;
6768
const computedEpoch = computeEpochAtSlot(data.slot);
6869
const committeeCount = epochCtx.getCommitteeCountPerSlot(computedEpoch);
69-
if (!(data.index < committeeCount)) {
70-
throw new Error(
71-
"Attestation committee index not within current committee count: " +
72-
`committeeIndex=${data.index} committeeCount=${committeeCount}`
73-
);
74-
}
70+
7571
if (!(data.target.epoch === epochCtx.previousShuffling.epoch || data.target.epoch === epochCtx.epoch)) {
7672
throw new Error(
7773
"Attestation target epoch not in previous or current epoch: " +
@@ -93,12 +89,45 @@ export function validateAttestation(
9389
);
9490
}
9591

96-
const committee = epochCtx.getBeaconCommittee(data.slot, data.index);
97-
if (attestation.aggregationBits.bitLen !== committee.length) {
98-
throw new Error(
99-
"Attestation aggregation bits length does not match committee length: " +
100-
`aggregationBitsLength=${attestation.aggregationBits.bitLen} committeeLength=${committee.length}`
92+
if (fork >= ForkSeq.electra) {
93+
assert.equal(data.index, 0, `AttestationData.index must be zero: index=${data.index}`);
94+
const attestationElectra = attestation as electra.Attestation;
95+
const committeeBitsLength = attestationElectra.committeeBits.bitLen;
96+
97+
if (committeeBitsLength > committeeCount) {
98+
throw new Error(
99+
`Attestation committee bits length are longer than number of committees: committeeBitsLength=${committeeBitsLength} numCommittees=${committeeCount}`
100+
);
101+
}
102+
103+
// TODO Electra: this should be obsolete soon when the spec switches to committeeIndices
104+
const committeeIndices = attestationElectra.committeeBits.getTrueBitIndexes();
105+
106+
// Get total number of attestation participant of every committee specified
107+
const participantCount = committeeIndices
108+
.map((committeeIndex) => epochCtx.getBeaconCommittee(data.slot, committeeIndex).length)
109+
.reduce((acc, committeeSize) => acc + committeeSize, 0);
110+
111+
assert.equal(
112+
attestationElectra.aggregationBits.bitLen,
113+
participantCount,
114+
`Attestation aggregation bits length does not match total number of committee participant aggregationBitsLength=${attestation.aggregationBits.bitLen} participantCount=${participantCount}`
101115
);
116+
} else {
117+
if (!(data.index < committeeCount)) {
118+
throw new Error(
119+
"Attestation committee index not within current committee count: " +
120+
`committeeIndex=${data.index} committeeCount=${committeeCount}`
121+
);
122+
}
123+
124+
const committee = epochCtx.getBeaconCommittee(data.slot, data.index);
125+
if (attestation.aggregationBits.bitLen !== committee.length) {
126+
throw new Error(
127+
"Attestation aggregation bits length does not match committee length: " +
128+
`aggregationBitsLength=${attestation.aggregationBits.bitLen} committeeLength=${committee.length}`
129+
);
130+
}
102131
}
103132
}
104133

packages/state-transition/src/block/processAttestations.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {phase0} from "@lodestar/types";
1+
import {allForks} from "@lodestar/types";
22
import {ForkSeq} from "@lodestar/params";
33
import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStatePhase0} from "../types.js";
44
import {processAttestationPhase0} from "./processAttestationPhase0.js";
@@ -10,7 +10,7 @@ import {processAttestationsAltair} from "./processAttestationsAltair.js";
1010
export function processAttestations(
1111
fork: ForkSeq,
1212
state: CachedBeaconStateAllForks,
13-
attestations: phase0.Attestation[],
13+
attestations: allForks.Attestation[],
1414
verifySignatures = true
1515
): void {
1616
if (fork === ForkSeq.phase0) {

packages/state-transition/src/block/processAttestationsAltair.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {byteArrayEquals} from "@chainsafe/ssz";
2-
import {Epoch, phase0} from "@lodestar/types";
2+
import {Epoch, allForks, phase0} from "@lodestar/types";
33
import {intSqrt} from "@lodestar/utils";
44

55
import {
@@ -32,7 +32,7 @@ const SLOTS_PER_EPOCH_SQRT = intSqrt(SLOTS_PER_EPOCH);
3232
export function processAttestationsAltair(
3333
fork: ForkSeq,
3434
state: CachedBeaconStateAltair,
35-
attestations: phase0.Attestation[],
35+
attestations: allForks.Attestation[],
3636
verifySignature = true
3737
): void {
3838
const {epochCtx} = state;
@@ -49,8 +49,7 @@ export function processAttestationsAltair(
4949
validateAttestation(fork, state, attestation);
5050

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

5554
// this check is done last because its the most expensive (if signature verification is toggled on)
5655
// TODO: Why should we verify an indexed attestation that we just created? If it's just for the signature

packages/state-transition/src/cache/epochCache.ts

+78-12
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@ import {CoordType, PublicKey} from "@chainsafe/bls/types";
22
import bls from "@chainsafe/bls";
33
import * as immutable from "immutable";
44
import {fromHexString} from "@chainsafe/ssz";
5-
import {BLSSignature, CommitteeIndex, Epoch, Slot, ValidatorIndex, phase0, SyncPeriod} from "@lodestar/types";
5+
import {
6+
BLSSignature,
7+
CommitteeIndex,
8+
Epoch,
9+
Slot,
10+
ValidatorIndex,
11+
phase0,
12+
SyncPeriod,
13+
allForks,
14+
electra,
15+
} from "@lodestar/types";
616
import {createBeaconConfig, BeaconConfig, ChainConfig} from "@lodestar/config";
717
import {
818
ATTESTATION_SUBNET_COUNT,
@@ -645,15 +655,47 @@ export class EpochCache {
645655
* Return the beacon committee at slot for index.
646656
*/
647657
getBeaconCommittee(slot: Slot, index: CommitteeIndex): Uint32Array {
658+
return this.getBeaconCommittees(slot, [index]);
659+
}
660+
661+
/**
662+
* Return a single Uint32Array representing concatted committees of indices
663+
*/
664+
getBeaconCommittees(slot: Slot, indices: CommitteeIndex[]): Uint32Array {
665+
if (indices.length === 0) {
666+
throw new Error("Attempt to get committees without providing CommitteeIndex");
667+
}
668+
648669
const slotCommittees = this.getShufflingAtSlot(slot).committees[slot % SLOTS_PER_EPOCH];
649-
if (index >= slotCommittees.length) {
650-
throw new EpochCacheError({
651-
code: EpochCacheErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE,
652-
index,
653-
maxIndex: slotCommittees.length,
654-
});
670+
const committees = [];
671+
672+
for (const index of indices) {
673+
if (index >= slotCommittees.length) {
674+
throw new EpochCacheError({
675+
code: EpochCacheErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE,
676+
index,
677+
maxIndex: slotCommittees.length,
678+
});
679+
}
680+
committees.push(slotCommittees[index]);
681+
}
682+
683+
// Early return if only one index
684+
if (committees.length === 1) {
685+
return committees[0];
686+
}
687+
688+
// Create a new Uint32Array to flatten `committees`
689+
const totalLength = committees.reduce((acc, curr) => acc + curr.length, 0);
690+
const result = new Uint32Array(totalLength);
691+
692+
let offset = 0;
693+
for (const committee of committees) {
694+
result.set(committee, offset);
695+
offset += committee.length;
655696
}
656-
return slotCommittees[index];
697+
698+
return result;
657699
}
658700

659701
getCommitteeCountPerSlot(epoch: Epoch): number {
@@ -739,10 +781,9 @@ export class EpochCache {
739781
/**
740782
* Return the indexed attestation corresponding to ``attestation``.
741783
*/
742-
getIndexedAttestation(attestation: phase0.Attestation): phase0.IndexedAttestation {
743-
const {aggregationBits, data} = attestation;
744-
const committeeIndices = this.getBeaconCommittee(data.slot, data.index);
745-
const attestingIndices = aggregationBits.intersectValues(committeeIndices);
784+
getIndexedAttestation(fork: ForkSeq, attestation: allForks.Attestation): allForks.IndexedAttestation {
785+
const {data} = attestation;
786+
const attestingIndices = this.getAttestingIndices(fork, attestation);
746787

747788
// sort in-place
748789
attestingIndices.sort((a, b) => a - b);
@@ -753,6 +794,31 @@ export class EpochCache {
753794
};
754795
}
755796

797+
/**
798+
* Return indices of validators who attestested in `attestation`
799+
*/
800+
getAttestingIndices(fork: ForkSeq, attestation: allForks.Attestation): number[] {
801+
if (fork < ForkSeq.electra) {
802+
const {aggregationBits, data} = attestation;
803+
const validatorIndices = this.getBeaconCommittee(data.slot, data.index);
804+
805+
return aggregationBits.intersectValues(validatorIndices);
806+
} else {
807+
const {aggregationBits, committeeBits, data} = attestation as electra.Attestation;
808+
809+
// There is a naming conflict on the term `committeeIndices`
810+
// In Lodestar it usually means a list of validator indices of participants in a committee
811+
// In the spec it means a list of committee indices according to committeeBits
812+
// This `committeeIndices` refers to the latter
813+
// TODO Electra: resolve the naming conflicts
814+
const committeeIndices = committeeBits.getTrueBitIndexes();
815+
816+
const validatorIndices = this.getBeaconCommittees(data.slot, committeeIndices);
817+
818+
return aggregationBits.intersectValues(validatorIndices);
819+
}
820+
}
821+
756822
getCommitteeAssignments(
757823
epoch: Epoch,
758824
requestedValidatorIndices: ValidatorIndex[]

packages/state-transition/src/signatureSets/indexedAttestation.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ export function getAttestationsSignatureSets(
4141
state: CachedBeaconStateAllForks,
4242
signedBlock: allForks.SignedBeaconBlock
4343
): ISignatureSet[] {
44+
// TODO: figure how to get attesting indices of an attestation once per block processing
4445
return signedBlock.message.body.attestations.map((attestation) =>
45-
getIndexedAttestationSignatureSet(state, state.epochCtx.getIndexedAttestation(attestation))
46+
getIndexedAttestationSignatureSet(
47+
state,
48+
state.epochCtx.getIndexedAttestation(state.epochCtx.config.getForkSeq(signedBlock.message.slot), attestation)
49+
)
4650
);
4751
}

0 commit comments

Comments
 (0)