Skip to content

Commit 0a7048e

Browse files
ensi321g11tech
authored andcommitted
feat: beacon node process electra attestations EIP-7549 (#6738)
* Process attestations in block * Fix check-types * Address comments
1 parent 78aec6b commit 0a7048e

File tree

7 files changed

+139
-37
lines changed

7 files changed

+139
-37
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) {
@@ -120,7 +121,8 @@ export async function importBlock(
120121

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

126128
const attDataRoot = toRootHex(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

+44-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {Slot, phase0, ssz} from "@lodestar/types";
2-
3-
import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH, ForkSeq} from "@lodestar/params";
41
import {toRootHex} from "@lodestar/utils";
2+
import {Slot, allForks, electra, phase0, ssz} from "@lodestar/types";
3+
import {MIN_ATTESTATION_INCLUSION_DELAY, SLOTS_PER_EPOCH, ForkSeq} from "@lodestar/params";
4+
import {assert} from "@lodestar/utils";
55
import {computeEpochAtSlot} from "../util/index.js";
66
import {CachedBeaconStatePhase0, CachedBeaconStateAllForks} from "../types.js";
77
import {isValidIndexedAttestation} from "./index.js";
@@ -51,27 +51,22 @@ export function processAttestationPhase0(
5151
state.previousEpochAttestations.push(pendingAttestation);
5252
}
5353

54-
if (!isValidIndexedAttestation(state, epochCtx.getIndexedAttestation(attestation), verifySignature)) {
54+
if (!isValidIndexedAttestation(state, epochCtx.getIndexedAttestation(ForkSeq.phase0, attestation), verifySignature)) {
5555
throw new Error("Attestation is not valid");
5656
}
5757
}
5858

5959
export function validateAttestation(
6060
fork: ForkSeq,
6161
state: CachedBeaconStateAllForks,
62-
attestation: phase0.Attestation
62+
attestation: allForks.Attestation
6363
): void {
6464
const {epochCtx} = state;
6565
const slot = state.slot;
6666
const data = attestation.data;
6767
const computedEpoch = computeEpochAtSlot(data.slot);
6868
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-
}
69+
7570
if (!(data.target.epoch === epochCtx.previousShuffling.epoch || data.target.epoch === epochCtx.epoch)) {
7671
throw new Error(
7772
"Attestation target epoch not in previous or current epoch: " +
@@ -93,12 +88,45 @@ export function validateAttestation(
9388
);
9489
}
9590

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

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
@@ -1,7 +1,17 @@
11
import {PublicKey} from "@chainsafe/blst";
22
import * as immutable from "immutable";
33
import {fromHexString} from "@chainsafe/ssz";
4-
import {BLSSignature, CommitteeIndex, Epoch, Slot, ValidatorIndex, phase0, SyncPeriod} from "@lodestar/types";
4+
import {
5+
BLSSignature,
6+
CommitteeIndex,
7+
Epoch,
8+
Slot,
9+
ValidatorIndex,
10+
phase0,
11+
SyncPeriod,
12+
allForks,
13+
electra,
14+
} from "@lodestar/types";
515
import {createBeaconConfig, BeaconConfig, ChainConfig} from "@lodestar/config";
616
import {
717
ATTESTATION_SUBNET_COUNT,
@@ -651,15 +661,47 @@ export class EpochCache {
651661
* Return the beacon committee at slot for index.
652662
*/
653663
getBeaconCommittee(slot: Slot, index: CommitteeIndex): Uint32Array {
664+
return this.getBeaconCommittees(slot, [index]);
665+
}
666+
667+
/**
668+
* Return a single Uint32Array representing concatted committees of indices
669+
*/
670+
getBeaconCommittees(slot: Slot, indices: CommitteeIndex[]): Uint32Array {
671+
if (indices.length === 0) {
672+
throw new Error("Attempt to get committees without providing CommitteeIndex");
673+
}
674+
654675
const slotCommittees = this.getShufflingAtSlot(slot).committees[slot % SLOTS_PER_EPOCH];
655-
if (index >= slotCommittees.length) {
656-
throw new EpochCacheError({
657-
code: EpochCacheErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE,
658-
index,
659-
maxIndex: slotCommittees.length,
660-
});
676+
const committees = [];
677+
678+
for (const index of indices) {
679+
if (index >= slotCommittees.length) {
680+
throw new EpochCacheError({
681+
code: EpochCacheErrorCode.COMMITTEE_INDEX_OUT_OF_RANGE,
682+
index,
683+
maxIndex: slotCommittees.length,
684+
});
685+
}
686+
committees.push(slotCommittees[index]);
687+
}
688+
689+
// Early return if only one index
690+
if (committees.length === 1) {
691+
return committees[0];
692+
}
693+
694+
// Create a new Uint32Array to flatten `committees`
695+
const totalLength = committees.reduce((acc, curr) => acc + curr.length, 0);
696+
const result = new Uint32Array(totalLength);
697+
698+
let offset = 0;
699+
for (const committee of committees) {
700+
result.set(committee, offset);
701+
offset += committee.length;
661702
}
662-
return slotCommittees[index];
703+
704+
return result;
663705
}
664706

665707
getCommitteeCountPerSlot(epoch: Epoch): number {
@@ -745,10 +787,9 @@ export class EpochCache {
745787
/**
746788
* Return the indexed attestation corresponding to ``attestation``.
747789
*/
748-
getIndexedAttestation(attestation: phase0.Attestation): phase0.IndexedAttestation {
749-
const {aggregationBits, data} = attestation;
750-
const committeeIndices = this.getBeaconCommittee(data.slot, data.index);
751-
const attestingIndices = aggregationBits.intersectValues(committeeIndices);
790+
getIndexedAttestation(fork: ForkSeq, attestation: allForks.Attestation): allForks.IndexedAttestation {
791+
const {data} = attestation;
792+
const attestingIndices = this.getAttestingIndices(fork, attestation);
752793

753794
// sort in-place
754795
attestingIndices.sort((a, b) => a - b);
@@ -759,6 +800,31 @@ export class EpochCache {
759800
};
760801
}
761802

803+
/**
804+
* Return indices of validators who attestested in `attestation`
805+
*/
806+
getAttestingIndices(fork: ForkSeq, attestation: allForks.Attestation): number[] {
807+
if (fork < ForkSeq.electra) {
808+
const {aggregationBits, data} = attestation;
809+
const validatorIndices = this.getBeaconCommittee(data.slot, data.index);
810+
811+
return aggregationBits.intersectValues(validatorIndices);
812+
} else {
813+
const {aggregationBits, committeeBits, data} = attestation as electra.Attestation;
814+
815+
// There is a naming conflict on the term `committeeIndices`
816+
// In Lodestar it usually means a list of validator indices of participants in a committee
817+
// In the spec it means a list of committee indices according to committeeBits
818+
// This `committeeIndices` refers to the latter
819+
// TODO Electra: resolve the naming conflicts
820+
const committeeIndices = committeeBits.getTrueBitIndexes();
821+
822+
const validatorIndices = this.getBeaconCommittees(data.slot, committeeIndices);
823+
824+
return aggregationBits.intersectValues(validatorIndices);
825+
}
826+
}
827+
762828
getCommitteeAssignments(
763829
epoch: Epoch,
764830
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: 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)