Skip to content

Commit

Permalink
Merge branch 'devnet-5' into nflaig/fix-attestation-format
Browse files Browse the repository at this point in the history
  • Loading branch information
nflaig committed Nov 29, 2024
2 parents 0b5bcb3 + 957684f commit 4d07fe0
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 54 deletions.
5 changes: 3 additions & 2 deletions packages/beacon-node/src/api/impl/beacon/pool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,16 @@ export function getBeaconPoolApi({
// when a validator is configured with multiple beacon node urls, this attestation data may come from another beacon node
// and the block hasn't been in our forkchoice since we haven't seen / processing that block
// see https://github.com/ChainSafe/lodestar/issues/5098
const {indexedAttestation, subnet, attDataRootHex, committeeIndex, aggregationBits} =
const {indexedAttestation, subnet, attDataRootHex, committeeIndex, committeeValidatorIndex, committeeSize} =
await validateGossipFnRetryUnknownRoot(validateFn, network, chain, slot, beaconBlockRoot);

if (network.shouldAggregate(subnet, slot)) {
const insertOutcome = chain.attestationPool.add(
committeeIndex,
attestation,
attDataRootHex,
aggregationBits
committeeValidatorIndex,
committeeSize
);
metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome});
}
Expand Down
21 changes: 12 additions & 9 deletions packages/beacon-node/src/chain/opPools/attestationPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ export class AttestationPool {
committeeIndex: CommitteeIndex,
attestation: SingleAttestation,
attDataRootHex: RootHex,
aggregationBits: BitArray | null
committeeValidatorIndex: number,
committeeSize: number
): InsertOutcome {
const slot = attestation.data.slot;
const fork = this.config.getForkName(slot);
Expand Down Expand Up @@ -149,10 +150,10 @@ export class AttestationPool {
const aggregate = aggregateByIndex.get(committeeIndex);
if (aggregate) {
// Aggregate mutating
return aggregateAttestationInto(aggregate, attestation, aggregationBits);
return aggregateAttestationInto(aggregate, attestation, committeeValidatorIndex);
}
// Create new aggregate
aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation, aggregationBits));
aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation, committeeValidatorIndex, committeeSize));
return InsertOutcome.NewData;
}

Expand Down Expand Up @@ -224,13 +225,12 @@ export class AttestationPool {
function aggregateAttestationInto(
aggregate: AggregateFast,
attestation: SingleAttestation,
aggregationBits: BitArray | null
committeeValidatorIndex: number
): InsertOutcome {
let bitIndex: number | null;

if (isElectraSingleAttestation(attestation)) {
assert.notNull(aggregationBits, "aggregationBits missing post-electra");
bitIndex = aggregationBits.getSingleTrueBit();
bitIndex = committeeValidatorIndex;
} else {
bitIndex = attestation.aggregationBits.getSingleTrueBit();
}
Expand All @@ -250,12 +250,15 @@ function aggregateAttestationInto(
/**
* Format `contribution` into an efficient `aggregate` to add more contributions in with aggregateContributionInto()
*/
function attestationToAggregate(attestation: SingleAttestation, aggregationBits: BitArray | null): AggregateFast {
function attestationToAggregate(
attestation: SingleAttestation,
committeeValidatorIndex: number,
committeeSize: number
): AggregateFast {
if (isElectraSingleAttestation(attestation)) {
assert.notNull(aggregationBits, "aggregationBits missing post-electra to generate aggregate");
return {
data: attestation.data,
aggregationBits,
aggregationBits: BitArray.fromSingleBit(committeeSize, committeeValidatorIndex),
committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, attestation.committeeIndex),
signature: signatureFromBytesNoCheck(attestation.signature),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ type AttDataBase64 = string;
export type AttestationDataCacheEntry = {
// part of shuffling data, so this does not take memory
committeeValidatorIndices: Uint32Array;
// TODO: remove this? this is available in SingleAttestation
committeeIndex: CommitteeIndex;
// IndexedAttestationData signing root, 32 bytes
signingRoot: Uint8Array;
Expand All @@ -21,8 +20,6 @@ export type AttestationDataCacheEntry = {
// for example in a mainnet node subscribing to all subnets, attestations are processed up to 20k per slot
attestationData: phase0.AttestationData;
subnet: number;
// aggregationBits only populates post-electra. Pre-electra can use get it directly from attestationOrBytes
aggregationBits: BitArray | null;
};

export enum RejectReason {
Expand Down
53 changes: 29 additions & 24 deletions packages/beacon-node/src/chain/validation/attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ import {
getAggregationBitsFromAttestationSerialized,
getAttDataFromSignedAggregateAndProofElectra,
getAttDataFromSignedAggregateAndProofPhase0,
getBeaconAttestationGossipIndex,
getCommitteeBitsFromSignedAggregateAndProofElectra,
getAttesterIndexFromSingleAttestationSerialized,
getCommitteeIndexFromSingleAttestationSerialized,
getSignatureFromAttestationSerialized,
getSignatureFromSingleAttestationSerialized,
} from "../../util/sszBytes.js";
import {Result, wrapError} from "../../util/wrapError.js";
import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/index.js";
Expand All @@ -68,7 +68,8 @@ export type AttestationValidationResult = {
subnet: number;
attDataRootHex: RootHex;
committeeIndex: CommitteeIndex;
aggregationBits: BitArray | null; // Field populated post-electra only
committeeValidatorIndex: number;
committeeSize: number;
};

export type AttestationOrBytes = ApiAttestation | GossipAttestation;
Expand Down Expand Up @@ -324,6 +325,7 @@ async function validateAttestationNoSignatureCheck(
}

let aggregationBits: BitArray | null = null;
let committeeValidatorIndex: number | null = 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).
Expand All @@ -343,11 +345,7 @@ async function validateAttestationNoSignatureCheck(
code: AttestationErrorCode.NOT_EXACTLY_ONE_AGGREGATION_BIT_SET,
});
}
} else {
// Populate aggregationBits if cached post-electra, else we populate later
if (attestationOrCache.cache && attestationOrCache.cache.aggregationBits !== null) {
aggregationBits = attestationOrCache.cache.aggregationBits;
}
committeeValidatorIndex = bitIndex;
}

let committeeValidatorIndices: Uint32Array;
Expand Down Expand Up @@ -406,10 +404,9 @@ async function validateAttestationNoSignatureCheck(
if (!isForkPostElectra(fork)) {
// The validity of aggregation bits are already checked above
assert.notNull(aggregationBits);
const bitIndex = aggregationBits.getSingleTrueBit();
assert.notNull(bitIndex);
assert.notNull(committeeValidatorIndex);

validatorIndex = committeeValidatorIndices[bitIndex];
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.
Expand All @@ -419,20 +416,26 @@ async function validateAttestationNoSignatureCheck(
});
}
} else {
validatorIndex = (attestationOrCache.attestation as SingleAttestation<ForkPostElectra>).attesterIndex;
// [REJECT] The attester is a member of the committee -- i.e.
// `attestation.attester_index in get_beacon_committee(state, attestation.data.slot, index)`.
// If `aggregationBitsElectra` exists, that means we have already cached it. No need to check again
if (aggregationBits === null) {
// Position of the validator in its committee
const committeeValidatorIndex = committeeValidatorIndices.indexOf(validatorIndex);
if (committeeValidatorIndex === -1) {
if (attestationOrCache.attestation) {
validatorIndex = (attestationOrCache.attestation as SingleAttestation<ForkPostElectra>).attesterIndex;
} else {
const attesterIndex = getAttesterIndexFromSingleAttestationSerialized(attestationOrCache.serializedData);
if (attesterIndex === null) {
throw new AttestationError(GossipAction.REJECT, {
code: AttestationErrorCode.ATTESTER_NOT_IN_COMMITTEE,
code: AttestationErrorCode.INVALID_SERIALIZED_BYTES,
});
}
validatorIndex = attesterIndex;
}

aggregationBits = BitArray.fromSingleBit(committeeValidatorIndices.length, committeeValidatorIndex);
// [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,
});
}
}

Expand Down Expand Up @@ -469,7 +472,9 @@ async function validateAttestationNoSignatureCheck(
let attDataRootHex: RootHex;
const signature = attestationOrCache.attestation
? attestationOrCache.attestation.signature
: getSignatureFromAttestationSerialized(attestationOrCache.serializedData);
: !isForkPostElectra(fork)
? getSignatureFromAttestationSerialized(attestationOrCache.serializedData)
: getSignatureFromSingleAttestationSerialized(attestationOrCache.serializedData);
if (signature === null) {
throw new AttestationError(GossipAction.REJECT, {
code: AttestationErrorCode.INVALID_SERIALIZED_BYTES,
Expand Down Expand Up @@ -503,7 +508,6 @@ async function validateAttestationNoSignatureCheck(
// root of AttestationData was already cached during getIndexedAttestationSignatureSet
attDataRootHex,
attestationData: attData,
aggregationBits: isForkPostElectra(fork) ? aggregationBits : null,
});
}
}
Expand Down Expand Up @@ -538,7 +542,8 @@ async function validateAttestationNoSignatureCheck(
signatureSet,
validatorIndex,
committeeIndex,
aggregationBits: isForkPostElectra(fork) ? aggregationBits : null,
committeeValidatorIndex,
committeeSize: committeeValidatorIndices.length,
};
}

Expand Down
13 changes: 10 additions & 3 deletions packages/beacon-node/src/network/processor/gossipHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,8 +637,14 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp
results.push(null);

// Handler
const {indexedAttestation, attDataRootHex, attestation, committeeIndex, aggregationBits} =
validationResult.result;
const {
indexedAttestation,
attDataRootHex,
attestation,
committeeIndex,
committeeValidatorIndex,
committeeSize,
} = validationResult.result;
metrics?.registerGossipUnaggregatedAttestation(gossipHandlerParams[i].seenTimestampSec, indexedAttestation);

try {
Expand All @@ -650,7 +656,8 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp
committeeIndex,
attestation,
attDataRootHex,
aggregationBits
committeeValidatorIndex,
committeeSize
);
metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome});
}
Expand Down
16 changes: 15 additions & 1 deletion packages/beacon-node/src/util/sszBytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
MAX_COMMITTEES_PER_SLOT,
isForkPostElectra,
} from "@lodestar/params";
import {BLSSignature, CommitteeIndex, RootHex, Slot} from "@lodestar/types";
import {BLSSignature, CommitteeIndex, RootHex, Slot, ValidatorIndex} from "@lodestar/types";

export type BlockRootHex = RootHex;
// pre-electra, AttestationData is used to cache attestations
Expand Down Expand Up @@ -54,6 +54,7 @@ const SIGNATURE_SIZE = 96;
const SINGLE_ATTESTATION_ATTDATA_OFFSET = 8 + 8;
const SINGLE_ATTESTATION_SLOT_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET;
const SINGLE_ATTESTATION_COMMITTEE_INDEX_OFFSET = 0;
const SINGLE_ATTESTATION_ATTESTER_INDEX_OFFSET = 8;
const SINGLE_ATTESTATION_BEACON_BLOCK_ROOT_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET + 8 + 8;
const SINGLE_ATTESTATION_SIGNATURE_OFFSET = SINGLE_ATTESTATION_ATTDATA_OFFSET + ATTESTATION_DATA_SIZE;
const SINGLE_ATTESTATION_SIZE = SINGLE_ATTESTATION_SIGNATURE_OFFSET + SIGNATURE_SIZE;
Expand Down Expand Up @@ -201,6 +202,19 @@ export function getCommitteeIndexFromSingleAttestationSerialized(
return getSlotFromOffset(data, VARIABLE_FIELD_OFFSET + SLOT_SIZE);
}

/**
* Extract attester index from SingleAttestation serialized bytes.
* Return null if data is not long enough to extract index.
* TODO Electra: Rename getSlotFromOffset to reflect generic usage
*/
export function getAttesterIndexFromSingleAttestationSerialized(data: Uint8Array): ValidatorIndex | null {
if (data.length !== SINGLE_ATTESTATION_SIZE) {
return null;
}

return getSlotFromOffset(data, SINGLE_ATTESTATION_ATTESTER_INDEX_OFFSET);
}

/**
* Extract block root from SingleAttestation serialized bytes.
* Return null if data is not long enough to extract block root.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ describe("AttestationPool", () => {
const clockStub = getMockedClock();
vi.spyOn(clockStub, "secFromSlot").mockReturnValue(0);

const committeeValidatorIndex = 0;
const committeeSize = 128;

const cutOffSecFromSlot = (2 / 3) * config.SECONDS_PER_SLOT;

// Mock attestations
Expand All @@ -37,10 +40,10 @@ describe("AttestationPool", () => {
signature: validSignature,
};
const electraAttestation: electra.Attestation = {
...ssz.electra.Attestation.defaultValue(),
committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, electraSingleAttestation.committeeIndex),
aggregationBits: BitArray.fromSingleBit(committeeSize, committeeValidatorIndex),
data: electraAttestationData,
signature: validSignature,
committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, electraSingleAttestation.committeeIndex),
};
const phase0AttestationData: phase0.AttestationData = {
...ssz.phase0.AttestationData.defaultValue(),
Expand All @@ -60,9 +63,14 @@ describe("AttestationPool", () => {

it("add correct electra attestation", () => {
const committeeIndex = 0;
const aggregationBits = ssz.electra.Attestation.fields.aggregationBits.defaultValue();
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraSingleAttestation.data));
const outcome = pool.add(committeeIndex, electraSingleAttestation, attDataRootHex, aggregationBits);
const outcome = pool.add(
committeeIndex,
electraSingleAttestation,
attDataRootHex,
committeeValidatorIndex,
committeeSize
);

expect(outcome).equal(InsertOutcome.NewData);
expect(pool.getAggregate(electraAttestationData.slot, committeeIndex, attDataRootHex)).toEqual(electraAttestation);
Expand All @@ -71,7 +79,7 @@ describe("AttestationPool", () => {
it("add correct phase0 attestation", () => {
const committeeIndex = null;
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(phase0Attestation.data));
const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex, null);
const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex, committeeValidatorIndex, committeeSize);

expect(outcome).equal(InsertOutcome.NewData);
expect(pool.getAggregate(phase0AttestationData.slot, committeeIndex, attDataRootHex)).toEqual(phase0Attestation);
Expand All @@ -82,18 +90,18 @@ describe("AttestationPool", () => {

it("add electra attestation without committee index", () => {
const committeeIndex = null;
const aggregationBits = ssz.electra.Attestation.fields.aggregationBits.defaultValue();
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraSingleAttestation.data));

expect(() => pool.add(committeeIndex, electraSingleAttestation, attDataRootHex, aggregationBits)).toThrow();
expect(() =>
pool.add(committeeIndex, electraSingleAttestation, attDataRootHex, committeeValidatorIndex, committeeSize)
).toThrow();
expect(pool.getAggregate(electraAttestationData.slot, committeeIndex, attDataRootHex)).toBeNull();
});

it("add phase0 attestation with committee index", () => {
const committeeIndex = 0;
const aggregationBits = ssz.electra.Attestation.fields.aggregationBits.defaultValue();
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(phase0Attestation.data));
const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex, aggregationBits);
const outcome = pool.add(committeeIndex, phase0Attestation, attDataRootHex, committeeValidatorIndex, committeeSize);

expect(outcome).equal(InsertOutcome.NewData);
expect(pool.getAggregate(phase0AttestationData.slot, committeeIndex, attDataRootHex)).toEqual(phase0Attestation);
Expand All @@ -104,15 +112,14 @@ describe("AttestationPool", () => {

it("add electra attestation with phase0 slot", () => {
const electraAttestationDataWithPhase0Slot = {...ssz.phase0.AttestationData.defaultValue(), slot: GENESIS_SLOT};
const aggregationBits = ssz.electra.Attestation.fields.aggregationBits.defaultValue();
const singleAttestation = {
...ssz.electra.SingleAttestation.defaultValue(),
data: electraAttestationDataWithPhase0Slot,
signature: validSignature,
};
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(electraAttestationDataWithPhase0Slot));

expect(() => pool.add(0, singleAttestation, attDataRootHex, aggregationBits)).toThrow();
expect(() => pool.add(0, singleAttestation, attDataRootHex, committeeValidatorIndex, committeeSize)).toThrow();
});

it("add phase0 attestation with electra slot", () => {
Expand All @@ -127,6 +134,6 @@ describe("AttestationPool", () => {
};
const attDataRootHex = toHexString(ssz.phase0.AttestationData.hashTreeRoot(phase0AttestationDataWithElectraSlot));

expect(() => pool.add(0, attestation, attDataRootHex, null)).toThrow();
expect(() => pool.add(0, attestation, attDataRootHex, committeeValidatorIndex, committeeSize)).toThrow();
});
});
Loading

0 comments on commit 4d07fe0

Please sign in to comment.