Skip to content

Commit

Permalink
Merge 9ad1e26 into 06b4c2d
Browse files Browse the repository at this point in the history
  • Loading branch information
twoeths authored Nov 3, 2024
2 parents 06b4c2d + 9ad1e26 commit 6b4648d
Show file tree
Hide file tree
Showing 28 changed files with 670 additions and 222 deletions.
43 changes: 27 additions & 16 deletions packages/api/src/beacon/routes/beacon/pool.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import {ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {isForkPostElectra} from "@lodestar/params";
import {AttesterSlashing, CommitteeIndex, Slot, capella, electra, phase0, ssz} from "@lodestar/types";
import {ForkPostElectra, ForkPreElectra, isForkPostElectra} from "@lodestar/params";
import {
AttesterSlashing,
CommitteeIndex,
SingleAttestation,
Slot,
capella,
electra,
phase0,
ssz,
} from "@lodestar/types";
import {
ArrayOf,
EmptyArgs,
Expand All @@ -20,6 +29,8 @@ import {MetaHeader, VersionCodec, VersionMeta} from "../../../utils/metadata.js"

// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes

const SingleAttestationListTypePhase0 = ArrayOf(ssz.phase0.Attestation);
const SingleAttestationListTypeElectra = ArrayOf(ssz.electra.SingleAttestation);
const AttestationListTypePhase0 = ArrayOf(ssz.phase0.Attestation);
const AttestationListTypeElectra = ArrayOf(ssz.electra.Attestation);
const AttesterSlashingListTypePhase0 = ArrayOf(ssz.phase0.AttesterSlashing);
Expand Down Expand Up @@ -142,7 +153,7 @@ export type Endpoints = {
*/
submitPoolAttestations: Endpoint<
"POST",
{signedAttestations: AttestationListPhase0},
{signedAttestations: SingleAttestation<ForkPreElectra>[]},
{body: unknown},
EmptyResponseData,
EmptyMeta
Expand All @@ -158,7 +169,7 @@ export type Endpoints = {
*/
submitPoolAttestationsV2: Endpoint<
"POST",
{signedAttestations: AttestationList},
{signedAttestations: SingleAttestation[]},
{body: unknown; headers: {[MetaHeader.Version]: string}},
EmptyResponseData,
EmptyMeta
Expand Down Expand Up @@ -316,10 +327,10 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
url: "/eth/v1/beacon/pool/attestations",
method: "POST",
req: {
writeReqJson: ({signedAttestations}) => ({body: AttestationListTypePhase0.toJson(signedAttestations)}),
parseReqJson: ({body}) => ({signedAttestations: AttestationListTypePhase0.fromJson(body)}),
writeReqSsz: ({signedAttestations}) => ({body: AttestationListTypePhase0.serialize(signedAttestations)}),
parseReqSsz: ({body}) => ({signedAttestations: AttestationListTypePhase0.deserialize(body)}),
writeReqJson: ({signedAttestations}) => ({body: SingleAttestationListTypePhase0.toJson(signedAttestations)}),
parseReqJson: ({body}) => ({signedAttestations: SingleAttestationListTypePhase0.fromJson(body)}),
writeReqSsz: ({signedAttestations}) => ({body: SingleAttestationListTypePhase0.serialize(signedAttestations)}),
parseReqSsz: ({body}) => ({signedAttestations: SingleAttestationListTypePhase0.deserialize(body)}),
schema: {
body: Schema.ObjectArray,
},
Expand All @@ -334,34 +345,34 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
const fork = config.getForkName(signedAttestations[0]?.data.slot ?? 0);
return {
body: isForkPostElectra(fork)
? AttestationListTypeElectra.toJson(signedAttestations as AttestationListElectra)
: AttestationListTypePhase0.toJson(signedAttestations as AttestationListPhase0),
? SingleAttestationListTypeElectra.toJson(signedAttestations as SingleAttestation<ForkPostElectra>[])
: SingleAttestationListTypePhase0.toJson(signedAttestations as SingleAttestation<ForkPreElectra>[]),
headers: {[MetaHeader.Version]: fork},
};
},
parseReqJson: ({body, headers}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedAttestations: isForkPostElectra(fork)
? AttestationListTypeElectra.fromJson(body)
: AttestationListTypePhase0.fromJson(body),
? SingleAttestationListTypeElectra.fromJson(body)
: SingleAttestationListTypePhase0.fromJson(body),
};
},
writeReqSsz: ({signedAttestations}) => {
const fork = config.getForkName(signedAttestations[0]?.data.slot ?? 0);
return {
body: isForkPostElectra(fork)
? AttestationListTypeElectra.serialize(signedAttestations as AttestationListElectra)
: AttestationListTypePhase0.serialize(signedAttestations as AttestationListPhase0),
? SingleAttestationListTypeElectra.serialize(signedAttestations as SingleAttestation<ForkPostElectra>[])
: SingleAttestationListTypePhase0.serialize(signedAttestations as SingleAttestation<ForkPreElectra>[]),
headers: {[MetaHeader.Version]: fork},
};
},
parseReqSsz: ({body, headers}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedAttestations: isForkPostElectra(fork)
? AttestationListTypeElectra.deserialize(body)
: AttestationListTypePhase0.deserialize(body),
? SingleAttestationListTypeElectra.deserialize(body)
: SingleAttestationListTypePhase0.deserialize(body),
};
},
schema: {
Expand Down
6 changes: 6 additions & 0 deletions packages/api/src/beacon/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
UintNum64,
altair,
capella,
electra,
phase0,
ssz,
sszTypesFor,
Expand Down Expand Up @@ -51,6 +52,8 @@ export enum EventType {
block = "block",
/** The node has received a valid attestation (from P2P or API) */
attestation = "attestation",
/** The node has received a valid SingleAttestation (from P2P or API) */
singleAttestation = "single_attestation",
/** The node has received a valid voluntary exit (from P2P or API) */
voluntaryExit = "voluntary_exit",
/** The node has received a valid proposer slashing (from P2P or API) */
Expand Down Expand Up @@ -79,6 +82,7 @@ export const eventTypes: {[K in EventType]: K} = {
[EventType.head]: EventType.head,
[EventType.block]: EventType.block,
[EventType.attestation]: EventType.attestation,
[EventType.singleAttestation]: EventType.singleAttestation,
[EventType.voluntaryExit]: EventType.voluntaryExit,
[EventType.proposerSlashing]: EventType.proposerSlashing,
[EventType.attesterSlashing]: EventType.attesterSlashing,
Expand Down Expand Up @@ -108,6 +112,7 @@ export type EventData = {
executionOptimistic: boolean;
};
[EventType.attestation]: Attestation;
[EventType.singleAttestation]: electra.SingleAttestation;
[EventType.voluntaryExit]: phase0.SignedVoluntaryExit;
[EventType.proposerSlashing]: phase0.ProposerSlashing;
[EventType.attesterSlashing]: AttesterSlashing;
Expand Down Expand Up @@ -237,6 +242,7 @@ export function getTypeByEvent(config: ChainForkConfig): {[K in EventType]: Type
return sszTypesFor(fork).Attestation.fromJson(attestation);
},
},
[EventType.singleAttestation]: ssz.electra.SingleAttestation,
[EventType.voluntaryExit]: ssz.phase0.SignedVoluntaryExit,
[EventType.proposerSlashing]: ssz.phase0.ProposerSlashing,
[EventType.attesterSlashing]: {
Expand Down
13 changes: 13 additions & 0 deletions packages/api/test/unit/beacon/testData/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ export const eventTestData: EventData = {
target: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"},
},
}),
[EventType.singleAttestation]: ssz.electra.SingleAttestation.fromJson({
committee_index: "1",
attester_index: "1",
data: {
slot: "1",
index: "1",
beacon_block_root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
source: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"},
target: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"},
},
signature:
"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
}),
[EventType.voluntaryExit]: ssz.phase0.SignedVoluntaryExit.fromJson({
message: {epoch: "1", validator_index: "1"},
signature:
Expand Down
45 changes: 33 additions & 12 deletions packages/beacon-node/src/api/impl/beacon/pool/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import {routes} from "@lodestar/api";
import {ApplicationMethods} from "@lodestar/api/server";
import {ForkName, SYNC_COMMITTEE_SUBNET_SIZE, isForkPostElectra} from "@lodestar/params";
import {Attestation, Epoch, isElectraAttestation, ssz} from "@lodestar/types";
import {
ForkName,
ForkPostElectra,
ForkPreElectra,
SYNC_COMMITTEE_SUBNET_SIZE,
isForkPostElectra,
} from "@lodestar/params";
import {Attestation, Epoch, SingleAttestation, isElectraAttestation, ssz} from "@lodestar/types";
import {
AttestationError,
AttestationErrorCode,
Expand All @@ -10,7 +16,7 @@ import {
} from "../../../../chain/errors/index.js";
import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js";
import {validateApiBlsToExecutionChange} from "../../../../chain/validation/blsToExecutionChange.js";
import {validateApiAttestation} from "../../../../chain/validation/index.js";
import {toElectraSingleAttestation, validateApiAttestation} from "../../../../chain/validation/index.js";
import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js";
import {validateApiSyncCommittee} from "../../../../chain/validation/syncCommittee.js";
import {validateApiVoluntaryExit} from "../../../../chain/validation/voluntaryExit.js";
Expand Down Expand Up @@ -99,20 +105,35 @@ 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} = await validateGossipFnRetryUnknownRoot(
validateFn,
network,
chain,
slot,
beaconBlockRoot
);
const {indexedAttestation, subnet, attDataRootHex, committeeIndex, aggregationBits, committeeBits} =
await validateGossipFnRetryUnknownRoot(validateFn, network, chain, slot, beaconBlockRoot);

if (network.shouldAggregate(subnet, slot)) {
const insertOutcome = chain.attestationPool.add(committeeIndex, attestation, attDataRootHex);
const insertOutcome = chain.attestationPool.add(
committeeIndex,
attestation,
attDataRootHex,
aggregationBits,
committeeBits
);
metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome});
}

chain.emitter.emit(routes.events.EventType.attestation, attestation);
if (isForkPostElectra(fork)) {
chain.emitter.emit(
routes.events.EventType.singleAttestation,
attestation as SingleAttestation<ForkPostElectra>
);
} else {
chain.emitter.emit(routes.events.EventType.attestation, attestation as SingleAttestation<ForkPreElectra>);
chain.emitter.emit(
routes.events.EventType.singleAttestation,
toElectraSingleAttestation(
attestation as SingleAttestation<ForkPreElectra>,
indexedAttestation.attestingIndices[0]
)
);
}

const sentPeers = await network.publishBeaconAttestation(attestation, subnet);
metrics?.onPoolSubmitUnaggregatedAttestation(seenTimestampSec, indexedAttestation, subnet, sentPeers);
Expand Down
7 changes: 6 additions & 1 deletion packages/beacon-node/src/chain/errors/attestationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ export enum AttestationErrorCode {
* Electra: Invalid attestationData index: is non-zero
*/
NON_ZERO_ATTESTATION_DATA_INDEX = "ATTESTATION_ERROR_NON_ZERO_ATTESTATION_DATA_INDEX",
/**
* Electra: Attester not in committee
*/
ATTESTER_NOT_IN_COMMITTEE = "ATTESTATION_ERROR_ATTESTER_NOT_IN_COMMITTEE",
}

export type AttestationErrorType =
Expand Down Expand Up @@ -170,7 +174,8 @@ export type AttestationErrorType =
| {code: AttestationErrorCode.INVALID_SERIALIZED_BYTES}
| {code: AttestationErrorCode.TOO_MANY_SKIPPED_SLOTS; headBlockSlot: Slot; attestationSlot: Slot}
| {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET}
| {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX};
| {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}
| {code: AttestationErrorCode.ATTESTER_NOT_IN_COMMITTEE};

export class AttestationError extends GossipActionError<AttestationErrorType> {
getMetadata(): Record<string, string | number | null> {
Expand Down
48 changes: 35 additions & 13 deletions packages/beacon-node/src/chain/opPools/attestationPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {Signature, aggregateSignatures} from "@chainsafe/blst";
import {BitArray} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {isForkPostElectra} from "@lodestar/params";
import {Attestation, RootHex, Slot, isElectraAttestation} from "@lodestar/types";
import {Attestation, RootHex, SingleAttestation, Slot, isElectraSingleAttestation} from "@lodestar/types";
import {assert, MapDef} from "@lodestar/utils";
import {IClock} from "../../util/clock.js";
import {InsertOutcome, OpPoolError, OpPoolErrorCode} from "./types.js";
Expand Down Expand Up @@ -105,7 +105,13 @@ export class AttestationPool {
* - Valid committeeIndex
* - Valid data
*/
add(committeeIndex: CommitteeIndex, attestation: Attestation, attDataRootHex: RootHex): InsertOutcome {
add(
committeeIndex: CommitteeIndex,
attestation: SingleAttestation,
attDataRootHex: RootHex,
aggregationBits: BitArray | null,
committeeBits: BitArray | null
): InsertOutcome {
const slot = attestation.data.slot;
const fork = this.config.getForkName(slot);
const lowestPermissibleSlot = this.lowestPermissibleSlot;
Expand All @@ -129,9 +135,9 @@ export class AttestationPool {
if (isForkPostElectra(fork)) {
// Electra only: this should not happen because attestation should be validated before reaching this
assert.notNull(committeeIndex, "Committee index should not be null in attestation pool post-electra");
assert.true(isElectraAttestation(attestation), "Attestation should be type electra.Attestation");
assert.true(isElectraSingleAttestation(attestation), "Attestation should be type electra.SingleAttestation");
} else {
assert.true(!isElectraAttestation(attestation), "Attestation should be type phase0.Attestation");
assert.true(!isElectraSingleAttestation(attestation), "Attestation should be type phase0.Attestation");
committeeIndex = null; // For pre-electra, committee index info is encoded in attDataRootIndex
}

Expand All @@ -144,10 +150,10 @@ export class AttestationPool {
const aggregate = aggregateByIndex.get(committeeIndex);
if (aggregate) {
// Aggregate mutating
return aggregateAttestationInto(aggregate, attestation);
return aggregateAttestationInto(aggregate, attestation, aggregationBits);
}
// Create new aggregate
aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation));
aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation, aggregationBits, committeeBits));
return InsertOutcome.NewData;
}

Expand Down Expand Up @@ -216,8 +222,19 @@ export class AttestationPool {
/**
* Aggregate a new attestation into `aggregate` mutating it
*/
function aggregateAttestationInto(aggregate: AggregateFast, attestation: Attestation): InsertOutcome {
const bitIndex = attestation.aggregationBits.getSingleTrueBit();
function aggregateAttestationInto(
aggregate: AggregateFast,
attestation: SingleAttestation,
aggregationBits: BitArray | null
): InsertOutcome {
let bitIndex;

if (isElectraSingleAttestation(attestation)) {
assert.notNull(aggregationBits, "aggregationBits missing post-electra");
bitIndex = aggregationBits.getSingleTrueBit();
} else {
bitIndex = attestation.aggregationBits.getSingleTrueBit();
}

// Should never happen, attestations are verified against this exact condition before
assert.notNull(bitIndex, "Invalid attestation in pool, not exactly one bit set");
Expand All @@ -234,13 +251,18 @@ function aggregateAttestationInto(aggregate: AggregateFast, attestation: Attesta
/**
* Format `contribution` into an efficient `aggregate` to add more contributions in with aggregateContributionInto()
*/
function attestationToAggregate(attestation: Attestation): AggregateFast {
if (isElectraAttestation(attestation)) {
function attestationToAggregate(
attestation: SingleAttestation,
aggregationBits: BitArray | null,
committeeBits: BitArray | null
): AggregateFast {
if (isElectraSingleAttestation(attestation)) {
assert.notNull(aggregationBits, "aggregationBits missing post-electra to generate aggregate");
assert.notNull(committeeBits, "committeeBits missing post-electra to generate aggregate");
return {
data: attestation.data,
// clone because it will be mutated
aggregationBits: attestation.aggregationBits.clone(),
committeeBits: attestation.committeeBits,
aggregationBits,
committeeBits,
signature: signatureFromBytesNoCheck(attestation.signature),
};
}
Expand Down
Loading

0 comments on commit 6b4648d

Please sign in to comment.